@forgeax/engine-render 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (979) hide show
  1. package/README.md +268 -640
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts +2 -0
  4. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts +2 -0
  6. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts +2 -0
  8. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts.map +1 -0
  9. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +1 -1
  10. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts +6 -0
  11. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts.map +1 -1
  12. package/dist/{gpu-driven/view-reference.d.ts → __tests__/gpu-driven-view-reference.d.ts} +4 -4
  13. package/dist/__tests__/gpu-driven-view-reference.d.ts.map +1 -0
  14. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts.map +1 -1
  15. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts +2 -0
  16. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts.map +1 -0
  17. package/dist/__tests__/morph-feature-plan.unit.test.d.ts +2 -0
  18. package/dist/__tests__/morph-feature-plan.unit.test.d.ts.map +1 -0
  19. package/dist/__tests__/render-feature-plan.test-d.d.ts +2 -0
  20. package/dist/__tests__/render-feature-plan.test-d.d.ts.map +1 -0
  21. package/dist/__tests__/render-feature-plan.unit.test.d.ts +2 -0
  22. package/dist/__tests__/render-feature-plan.unit.test.d.ts.map +1 -0
  23. package/dist/__tests__/ssao-config.unit.test.d.ts +2 -0
  24. package/dist/__tests__/ssao-config.unit.test.d.ts.map +1 -0
  25. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts +2 -0
  26. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts.map +1 -0
  27. package/dist/__tests__/standard-descriptors.test-d.d.ts +2 -0
  28. package/dist/__tests__/standard-descriptors.test-d.d.ts.map +1 -0
  29. package/dist/__tests__/standard-pipeline.dawn.test.d.ts +2 -0
  30. package/dist/__tests__/standard-pipeline.dawn.test.d.ts.map +1 -0
  31. package/dist/__tests__/standard-pipeline.integration.test.d.ts +2 -0
  32. package/dist/__tests__/standard-pipeline.integration.test.d.ts.map +1 -0
  33. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts +2 -0
  34. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts.map +1 -0
  35. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts +2 -0
  36. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts.map +1 -0
  37. package/dist/{renderer/backend-selection.d.ts → assembly/backend-contract.d.ts} +15 -4
  38. package/dist/assembly/backend-contract.d.ts.map +1 -0
  39. package/dist/assembly/factory.d.ts +214 -0
  40. package/dist/assembly/factory.d.ts.map +1 -0
  41. package/dist/assembly/host-contract.d.ts +84 -0
  42. package/dist/assembly/host-contract.d.ts.map +1 -0
  43. package/dist/{renderer → assembly}/material/assembly.d.ts +0 -1
  44. package/dist/{renderer → assembly}/material/assembly.d.ts.map +1 -1
  45. package/dist/assembly/material/pipeline-projection.d.ts +30 -0
  46. package/dist/assembly/material/pipeline-projection.d.ts.map +1 -0
  47. package/dist/authoring.d.ts +11 -11
  48. package/dist/authoring.d.ts.map +1 -1
  49. package/dist/authoring.mjs +25 -3
  50. package/dist/authoring.mjs.map +1 -1
  51. package/dist/chunk-CQXZC5XI.mjs +182 -0
  52. package/dist/chunk-CQXZC5XI.mjs.map +1 -0
  53. package/dist/chunk-HCOGPHKS.mjs +93 -0
  54. package/dist/chunk-HCOGPHKS.mjs.map +1 -0
  55. package/dist/chunk-YEJIUW7D.mjs +1152 -0
  56. package/dist/chunk-YEJIUW7D.mjs.map +1 -0
  57. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +2 -0
  58. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +1 -0
  59. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +2 -0
  60. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.map +1 -0
  61. package/dist/components/__tests__/schema-contract.unit.test.d.ts +2 -0
  62. package/dist/components/__tests__/schema-contract.unit.test.d.ts.map +1 -0
  63. package/dist/components/glyph-text.d.ts +1 -1
  64. package/dist/components/glyph-text.d.ts.map +1 -1
  65. package/dist/components/index.d.ts +4 -2
  66. package/dist/components/index.d.ts.map +1 -1
  67. package/dist/components/instances.d.ts +1 -1
  68. package/dist/components/instances.d.ts.map +1 -1
  69. package/dist/components/light-helpers.d.ts +1 -1
  70. package/dist/components/light-helpers.d.ts.map +1 -1
  71. package/dist/components/lines.d.ts +5 -0
  72. package/dist/components/lines.d.ts.map +1 -0
  73. package/dist/components/mesh-filter.d.ts +7 -7
  74. package/dist/components/mesh-filter.d.ts.map +1 -1
  75. package/dist/components/points.d.ts +15 -0
  76. package/dist/components/points.d.ts.map +1 -0
  77. package/dist/components/scene-instance.d.ts +11 -11
  78. package/dist/components/skybox-background.d.ts +56 -0
  79. package/dist/components/skybox-background.d.ts.map +1 -0
  80. package/dist/components/skylight.d.ts +55 -0
  81. package/dist/components/skylight.d.ts.map +1 -0
  82. package/dist/components/sprite-animation.d.ts +2 -3
  83. package/dist/components/sprite-animation.d.ts.map +1 -1
  84. package/dist/components/sprite-instances.d.ts +1 -1
  85. package/dist/components/sprite-playback-mode.d.ts +6 -1
  86. package/dist/components/sprite-playback-mode.d.ts.map +1 -1
  87. package/dist/components/sprite-region-override.d.ts +2 -3
  88. package/dist/components/sprite-region-override.d.ts.map +1 -1
  89. package/dist/components/tile-layer.d.ts +5 -0
  90. package/dist/components/tile-layer.d.ts.map +1 -1
  91. package/dist/construct-renderer.d.ts +18 -1
  92. package/dist/construct-renderer.d.ts.map +1 -1
  93. package/dist/construct-renderer.mjs +27463 -4
  94. package/dist/construct-renderer.mjs.map +1 -1
  95. package/dist/debug-draw-glue.d.ts +2 -53
  96. package/dist/debug-draw-glue.d.ts.map +1 -1
  97. package/dist/device/device-scope.d.ts +83 -0
  98. package/dist/device/device-scope.d.ts.map +1 -0
  99. package/dist/{gpu-resource-store.d.ts → device/gpu-residency.d.ts} +91 -48
  100. package/dist/device/gpu-residency.d.ts.map +1 -0
  101. package/dist/device/resource-types.d.ts +8 -0
  102. package/dist/device/resource-types.d.ts.map +1 -0
  103. package/dist/engine-metrics.d.ts +2 -2
  104. package/dist/engine-metrics.d.ts.map +1 -1
  105. package/dist/errors/recover.d.ts +28 -1
  106. package/dist/errors/recover.d.ts.map +1 -1
  107. package/dist/errors/render.d.ts +320 -248
  108. package/dist/errors/render.d.ts.map +1 -1
  109. package/dist/features/fullscreen.d.ts +19 -0
  110. package/dist/features/fullscreen.d.ts.map +1 -0
  111. package/dist/features/host.d.ts +32 -26
  112. package/dist/features/host.d.ts.map +1 -1
  113. package/dist/features/morph/morph-feature.d.ts +39 -5
  114. package/dist/features/morph/morph-feature.d.ts.map +1 -1
  115. package/dist/features/plan.d.ts +182 -0
  116. package/dist/features/plan.d.ts.map +1 -0
  117. package/dist/features/prepared-gpu-work.d.ts +21 -17
  118. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  119. package/dist/features/prepared-graphics-store.d.ts +2 -1
  120. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  121. package/dist/features/prepared-graphics.d.ts +11 -21
  122. package/dist/features/prepared-graphics.d.ts.map +1 -1
  123. package/dist/features/render-graph-compute.d.ts +2 -2
  124. package/dist/features/render-graph-compute.d.ts.map +1 -1
  125. package/dist/features/render-graph-contribution.d.ts +4 -7
  126. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  127. package/dist/features/render-graph-raster.d.ts +7 -3
  128. package/dist/features/render-graph-raster.d.ts.map +1 -1
  129. package/dist/features/render-graph-resources.d.ts +6 -6
  130. package/dist/features/render-graph-resources.d.ts.map +1 -1
  131. package/dist/features/targets.d.ts +5 -5
  132. package/dist/features/targets.d.ts.map +1 -1
  133. package/dist/features/types.d.ts +11 -190
  134. package/dist/features/types.d.ts.map +1 -1
  135. package/dist/features/vocabulary.d.ts +14 -0
  136. package/dist/features/vocabulary.d.ts.map +1 -0
  137. package/dist/fullscreen-post-process-pass.d.ts +4 -25
  138. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  139. package/dist/glyph-text-layout-system.d.ts +3 -2
  140. package/dist/glyph-text-layout-system.d.ts.map +1 -1
  141. package/dist/gpu-driven/batch-topology.d.ts +6 -75
  142. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  143. package/dist/gpu-driven/production-raster.d.ts +13 -112
  144. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  145. package/dist/gpu-driven/view-gpu.d.ts +10 -66
  146. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  147. package/dist/gpu-resource.d.ts +3 -3
  148. package/dist/gpu-scene-schema.d.ts +1 -94
  149. package/dist/gpu-scene-schema.d.ts.map +1 -1
  150. package/dist/gpu-scene.d.ts +7 -33
  151. package/dist/gpu-scene.d.ts.map +1 -1
  152. package/dist/hdrp-buffers.d.ts +3 -53
  153. package/dist/hdrp-buffers.d.ts.map +1 -1
  154. package/dist/ibl/IblPipelineCache.d.ts +74 -180
  155. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  156. package/dist/ibl/skylight-bind-group.d.ts +1 -1
  157. package/dist/index.d.ts +17 -43
  158. package/dist/index.d.ts.map +1 -1
  159. package/dist/index.mjs +167 -3
  160. package/dist/index.mjs.map +1 -1
  161. package/dist/inspection-types.d.ts +72 -0
  162. package/dist/inspection-types.d.ts.map +1 -0
  163. package/dist/instance-buffer-cache.d.ts +1 -4
  164. package/dist/instance-buffer-cache.d.ts.map +1 -1
  165. package/dist/internal.d.ts +1 -73
  166. package/dist/internal.d.ts.map +1 -1
  167. package/dist/internal.mjs +1 -43
  168. package/dist/internal.mjs.map +1 -1
  169. package/dist/lifecycle.d.ts +36 -2
  170. package/dist/lifecycle.d.ts.map +1 -1
  171. package/dist/materials.d.ts +0 -3
  172. package/dist/materials.d.ts.map +1 -1
  173. package/dist/mesh-material-bindings.d.ts +0 -1
  174. package/dist/mesh-material-bindings.d.ts.map +1 -1
  175. package/dist/pbr-pipeline.d.ts +77 -29
  176. package/dist/pbr-pipeline.d.ts.map +1 -1
  177. package/dist/pipeline/standard-forward-lane.d.ts +5 -0
  178. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -0
  179. package/dist/pipeline/standard-pipeline.d.ts +31 -0
  180. package/dist/pipeline/standard-pipeline.d.ts.map +1 -0
  181. package/dist/pipeline/standard-profile.d.ts +40 -0
  182. package/dist/pipeline/standard-profile.d.ts.map +1 -0
  183. package/dist/pipeline-builder.d.ts +3 -3
  184. package/dist/pipeline-builder.d.ts.map +1 -1
  185. package/dist/pipeline-spec-types.d.ts +24 -0
  186. package/dist/pipeline-spec-types.d.ts.map +1 -0
  187. package/dist/pipeline-spec.d.ts +16 -155
  188. package/dist/pipeline-spec.d.ts.map +1 -1
  189. package/dist/plugin.d.ts +1 -14
  190. package/dist/plugin.d.ts.map +1 -1
  191. package/dist/points-lines/__tests__/bounds.unit.test.d.ts +2 -0
  192. package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +1 -0
  193. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +2 -0
  194. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +1 -0
  195. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +2 -0
  196. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +1 -0
  197. package/dist/points-lines/__tests__/inspection.unit.test.d.ts +2 -0
  198. package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +1 -0
  199. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +2 -0
  200. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +1 -0
  201. package/dist/points-lines/__tests__/points-lines.bench.d.ts +2 -0
  202. package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +1 -0
  203. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +2 -0
  204. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +1 -0
  205. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +2 -0
  206. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +1 -0
  207. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +2 -0
  208. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +1 -0
  209. package/dist/points-lines/admission.d.ts +36 -0
  210. package/dist/points-lines/admission.d.ts.map +1 -0
  211. package/dist/points-lines/bounds.d.ts +8 -0
  212. package/dist/points-lines/bounds.d.ts.map +1 -0
  213. package/dist/points-lines/expansion-cache.d.ts +28 -0
  214. package/dist/points-lines/expansion-cache.d.ts.map +1 -0
  215. package/dist/points-lines/inspection.d.ts +53 -0
  216. package/dist/points-lines/inspection.d.ts.map +1 -0
  217. package/dist/points-lines/prepare.d.ts +67 -0
  218. package/dist/points-lines/prepare.d.ts.map +1 -0
  219. package/dist/points-lines/record.d.ts +60 -0
  220. package/dist/points-lines/record.d.ts.map +1 -0
  221. package/dist/points-lines/snapshot.d.ts +49 -0
  222. package/dist/points-lines/snapshot.d.ts.map +1 -0
  223. package/dist/post-process-errors.d.ts +18 -18
  224. package/dist/post-process-errors.d.ts.map +1 -1
  225. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -5
  226. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  227. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts +2 -0
  228. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts.map +1 -0
  229. package/dist/record/frame-lighting.d.ts +7 -73
  230. package/dist/record/frame-lighting.d.ts.map +1 -1
  231. package/dist/record/frame-snapshot.d.ts +44 -44
  232. package/dist/record/frame-snapshot.d.ts.map +1 -1
  233. package/dist/record/frame-targets.d.ts +5 -38
  234. package/dist/record/frame-targets.d.ts.map +1 -1
  235. package/dist/record/frame.d.ts +64 -52
  236. package/dist/record/frame.d.ts.map +1 -1
  237. package/dist/record/helpers.d.ts +3 -2
  238. package/dist/record/helpers.d.ts.map +1 -1
  239. package/dist/record/main-pass-geometry.d.ts +12 -4
  240. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  241. package/dist/record/main-pass-material.d.ts +90 -59
  242. package/dist/record/main-pass-material.d.ts.map +1 -1
  243. package/dist/record/main-pass-sprite-draws.d.ts +28 -3
  244. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  245. package/dist/record/main-pass.d.ts +1 -1
  246. package/dist/record/main-pass.d.ts.map +1 -1
  247. package/dist/record/mesh-ssbo.d.ts +276 -3
  248. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  249. package/dist/record/render-context.d.ts +252 -0
  250. package/dist/record/render-context.d.ts.map +1 -0
  251. package/dist/record/shadow-pass.d.ts +15 -88
  252. package/dist/record/shadow-pass.d.ts.map +1 -1
  253. package/dist/record/skybox-post-pass.d.ts +1 -1
  254. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  255. package/dist/record/typed-frame-graph.d.ts +29 -7
  256. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  257. package/dist/record/view-ubo.d.ts +9 -3
  258. package/dist/record/view-ubo.d.ts.map +1 -1
  259. package/dist/render-contract.d.ts +424 -0
  260. package/dist/render-contract.d.ts.map +1 -0
  261. package/dist/render-data.d.ts.map +1 -1
  262. package/dist/render-graph-primitives.d.ts +14 -362
  263. package/dist/render-graph-primitives.d.ts.map +1 -1
  264. package/dist/render-pipeline.d.ts +25 -64
  265. package/dist/render-pipeline.d.ts.map +1 -1
  266. package/dist/render-system-extract.d.ts +110 -338
  267. package/dist/render-system-extract.d.ts.map +1 -1
  268. package/dist/render-system.d.ts +24 -713
  269. package/dist/render-system.d.ts.map +1 -1
  270. package/dist/scene/render-scene-types.d.ts +61 -0
  271. package/dist/scene/render-scene-types.d.ts.map +1 -0
  272. package/dist/scene/render-scene.d.ts +112 -0
  273. package/dist/scene/render-scene.d.ts.map +1 -0
  274. package/dist/scene-instances/post-spawn-resolve-joints.d.ts +2 -5
  275. package/dist/scene-instances/post-spawn-resolve-joints.d.ts.map +1 -1
  276. package/dist/ssao-buffers.d.ts +22 -3
  277. package/dist/ssao-buffers.d.ts.map +1 -1
  278. package/dist/systems/skin-palette-allocator.d.ts +2 -4
  279. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  280. package/dist/systems/skin-palette-types.d.ts +14 -0
  281. package/dist/systems/skin-palette-types.d.ts.map +1 -0
  282. package/dist/systems/transparent-sort-config.d.ts +23 -2
  283. package/dist/systems/transparent-sort-config.d.ts.map +1 -1
  284. package/dist/tilemap-chunk-extract-system.d.ts +4 -3
  285. package/dist/tilemap-chunk-extract-system.d.ts.map +1 -1
  286. package/dist/typed-render-graph-primitives.d.ts +2 -4
  287. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  288. package/dist/typed-shadow-passes.d.ts +3 -3
  289. package/dist/typed-shadow-passes.d.ts.map +1 -1
  290. package/package.json +23 -23
  291. package/src/__tests__/authoring-path.unit.test.ts +9 -0
  292. package/src/__tests__/backend-selection-rhi-capture.test-d.ts +1 -1
  293. package/src/__tests__/callback-inference.test-d.ts +6 -3
  294. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -4
  295. package/src/__tests__/construct-renderer-identity.unit.test.ts +15 -2
  296. package/src/__tests__/device-scope-lifecycle.unit.test.ts +151 -0
  297. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +42 -0
  298. package/src/__tests__/direct-extract-consumer-contract.test-d.ts +8 -19
  299. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -13
  300. package/src/__tests__/factory-contract.integration.test.ts +158 -209
  301. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +1 -4
  302. package/src/__tests__/frame-observation.contract.test.ts +17 -8
  303. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +32 -0
  304. package/src/__tests__/glyph-text-layout-recovery.unit.test.ts +6 -11
  305. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +21 -18
  306. package/src/__tests__/gpu-driven-production.integration.test.ts +32 -205
  307. package/src/__tests__/gpu-driven-scaling.unit.test.ts +4 -7
  308. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +87 -8
  309. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +170 -8
  310. package/src/{gpu-driven/view-reference.ts → __tests__/gpu-driven-view-reference.ts} +4 -3
  311. package/src/__tests__/gpu-driven-view.browser.test.ts +12 -1
  312. package/src/__tests__/gpu-driven-view.dawn.test.ts +12 -1
  313. package/src/__tests__/gpu-driven-view.unit.test.ts +11 -267
  314. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +3 -3
  315. package/src/__tests__/gpu-scene-render-graph-gpu.ts +2 -3
  316. package/src/__tests__/gpu-scene.dawn.test.ts +2 -6
  317. package/src/__tests__/gpu-scene.unit.test.ts +10 -24
  318. package/src/__tests__/ibl/face-uniforms.unit.test.ts +46 -0
  319. package/src/__tests__/infinite-grid-no-vertex-probe.unit.test.ts +34 -66
  320. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -5
  321. package/src/__tests__/light-buffer-layout.unit.test.ts +4 -49
  322. package/src/__tests__/material-cooked-projection.unit.test.ts +31 -58
  323. package/src/__tests__/material-snapshot-mutation.integration.test.ts +10 -15
  324. package/src/__tests__/material-static-dynamic-route.unit.test.ts +31 -50
  325. package/src/__tests__/material-stride-surface.unit.test.ts +2 -2
  326. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +7 -8
  327. package/src/__tests__/morph-culling-reentry.dawn.test.ts +29 -38
  328. package/src/__tests__/morph-feature-gpu-work.integration.test.ts +45 -43
  329. package/src/__tests__/morph-feature-plan.unit.test.ts +53 -0
  330. package/src/__tests__/morph-visual-evidence.integration.test.ts +47 -33
  331. package/src/__tests__/nineslice-handle-surface.unit.test.ts +11 -6
  332. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -13
  333. package/src/__tests__/prepared-graphics-capability.unit.test.ts +6 -81
  334. package/src/__tests__/prepared-graphics-generation.unit.test.ts +3 -17
  335. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +6 -74
  336. package/src/__tests__/prepared-graphics-resolver.unit.test.ts +1 -3
  337. package/src/__tests__/prepared-graphics-snapshot.unit.test.ts +2 -2
  338. package/src/__tests__/profiler-default-off.test.ts +1 -1
  339. package/src/__tests__/profiler-phase-catalog.test.ts +2 -2
  340. package/src/__tests__/profiler-phase-integration.test.ts +5 -13
  341. package/src/__tests__/profiler-phase-token.test.ts +3 -8
  342. package/src/__tests__/public-surface.test-d.ts +65 -3
  343. package/src/__tests__/recover-error-policy-owner.unit.test.ts +1 -21
  344. package/src/__tests__/render-error-code-owner.test-d.ts +24 -23
  345. package/src/__tests__/render-error-detail-ssot.test-d.ts +0 -95
  346. package/src/__tests__/render-error-exhaustive.test-d.ts +27 -1
  347. package/src/__tests__/render-feature-assembly.test-d.ts +8 -16
  348. package/src/__tests__/render-feature-context.test-d.ts +16 -40
  349. package/src/__tests__/render-feature-contract.test-d.ts +9 -18
  350. package/src/__tests__/render-feature-diagnostics.unit.test.ts +6 -8
  351. package/src/__tests__/render-feature-docs.unit.test.ts +8 -7
  352. package/src/__tests__/render-feature-gpu-work.integration.test.ts +14 -75
  353. package/src/__tests__/render-feature-plan.test-d.ts +161 -0
  354. package/src/__tests__/render-feature-plan.unit.test.ts +170 -0
  355. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +8 -19
  356. package/src/__tests__/render-feature-prepared-errors.test-d.ts +3 -6
  357. package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +10 -71
  358. package/src/__tests__/render-feature-prepared-graphics-isolation.unit.test.ts +11 -61
  359. package/src/__tests__/render-feature-prepared-graphics-negative.test-d.ts +20 -43
  360. package/src/__tests__/render-feature-prepared-graphics.test-d.ts +56 -78
  361. package/src/__tests__/render-feature-public-surface.test-d.ts +6 -24
  362. package/src/__tests__/render-feature-retry.unit.test.ts +7 -48
  363. package/src/__tests__/render-feature-scheduling.unit.test.ts +15 -27
  364. package/src/__tests__/render-feature-stage-failed-policy-owner.unit.test.ts +0 -9
  365. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +19 -109
  366. package/src/__tests__/render-feature-visibility-context.test-d.ts +1 -6
  367. package/src/__tests__/render-feature-visibility-context.unit.test.ts +1 -2
  368. package/src/__tests__/render-feature-zero-work.unit.test.ts +14 -20
  369. package/src/__tests__/render-scene-projection.unit.test.ts +73 -111
  370. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +82 -95
  371. package/src/__tests__/renderer-lifecycle.integration.test.ts +49 -1
  372. package/src/__tests__/shader-stage-surface.unit.test.ts +14 -9
  373. package/src/__tests__/ssao-config.unit.test.ts +36 -0
  374. package/src/__tests__/ssao-kernel-surface.unit.test.ts +4 -5
  375. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +28 -0
  376. package/src/__tests__/standard-descriptors.test-d.ts +22 -0
  377. package/src/__tests__/standard-pipeline.dawn.test.ts +61 -0
  378. package/src/__tests__/standard-pipeline.integration.test.ts +124 -0
  379. package/src/__tests__/standard-profile-oracle.unit.test.ts +62 -0
  380. package/src/__tests__/surface-configuration.unit.test.ts +1 -2
  381. package/src/__tests__/texture-usage-surface.unit.test.ts +3 -6
  382. package/src/__tests__/typed-pipeline-topology.unit.test.ts +40 -206
  383. package/src/__tests__/urp-pipeline-targets.unit.test.ts +1 -11
  384. package/src/__tests__/urp-pipeline.unit.test.ts +2 -2
  385. package/src/__tests__/vertex-color-pipeline.unit.test.ts +10 -2
  386. package/src/__tests__/vertex-layout-consumer.unit.test.ts +1 -1
  387. package/src/__tests__/visibility-diagnostics.integration.test.ts +7 -11
  388. package/src/__tests__/visibility-extract.integration.test.ts +20 -11
  389. package/src/__tests__/visibility-extract.perf.test.ts +66 -32
  390. package/src/__tests__/visibility-feature-dedup.integration.test.ts +2 -4
  391. package/src/__tests__/visibility-hidden-report.unit.test.ts +1 -2
  392. package/src/__tests__/visibility-instances.integration.test.ts +7 -11
  393. package/src/__tests__/visibility-multi-world.integration.test.ts +38 -6
  394. package/src/__tests__/visibility-producer-matrix.integration.test.ts +18 -41
  395. package/src/__tests__/visibility-public-surface.test-d.ts +10 -9
  396. package/src/__tests__/visibility-resolver.unit.test.ts +2 -1
  397. package/src/__tests__/visibility-resource-short-circuit.unit.test.ts +80 -0
  398. package/src/__tests__/visibility-snapshot-contract.unit.test.ts +2 -1
  399. package/src/__tests__/vocabulary.unit.test.ts +1 -1
  400. package/src/assembly/backend-contract.ts +57 -0
  401. package/src/{renderer/renderer-factory.ts → assembly/factory.ts} +1741 -2699
  402. package/src/assembly/host-contract.ts +101 -0
  403. package/src/{renderer → assembly}/material/assembly.ts +2 -7
  404. package/src/assembly/material/pipeline-projection.ts +63 -0
  405. package/src/authoring.ts +15 -22
  406. package/src/components/__tests__/points-lines-admission.unit.test.ts +219 -0
  407. package/src/components/__tests__/points-lines-schema.unit.test.ts +56 -0
  408. package/src/components/__tests__/schema-contract.unit.test.ts +10 -0
  409. package/src/components/glyph-text.ts +3 -2
  410. package/src/components/index.ts +14 -9
  411. package/src/components/instances.ts +4 -5
  412. package/src/components/light-helpers.ts +1 -1
  413. package/src/components/lines.ts +18 -0
  414. package/src/components/mesh-filter.ts +11 -10
  415. package/src/components/points.ts +40 -0
  416. package/src/components/post-process-params.ts +3 -3
  417. package/src/components/scene-instance.ts +12 -12
  418. package/src/components/skybox-background.ts +103 -0
  419. package/src/components/{environment.ts → skylight.ts} +2 -110
  420. package/src/components/sprite-animation.ts +3 -4
  421. package/src/components/sprite-instances.ts +2 -2
  422. package/src/components/sprite-playback-mode.ts +7 -1
  423. package/src/components/sprite-region-override.ts +2 -3
  424. package/src/components/tile-layer.ts +8 -2
  425. package/src/construct-renderer.ts +67 -4
  426. package/src/debug-draw-glue.ts +8 -129
  427. package/src/device/device-scope.ts +306 -0
  428. package/src/{gpu-resource-store.ts → device/gpu-residency.ts} +540 -639
  429. package/src/device/resource-types.ts +20 -0
  430. package/src/engine-metrics.ts +12 -19
  431. package/src/errors/recover.ts +45 -3
  432. package/src/errors/render.ts +469 -429
  433. package/src/features/fullscreen.ts +46 -0
  434. package/src/features/host.ts +575 -309
  435. package/src/features/morph/morph-feature.ts +195 -105
  436. package/src/features/plan.ts +447 -0
  437. package/src/features/prepared-gpu-work.ts +72 -15
  438. package/src/features/prepared-graphics-store.ts +20 -6
  439. package/src/features/prepared-graphics.ts +12 -71
  440. package/src/features/render-graph-compute.ts +14 -5
  441. package/src/features/render-graph-contribution.ts +43 -63
  442. package/src/features/render-graph-raster.ts +32 -8
  443. package/src/features/render-graph-resources.ts +25 -23
  444. package/src/features/targets.ts +8 -21
  445. package/src/features/types.ts +39 -227
  446. package/src/features/vocabulary.ts +24 -0
  447. package/src/fullscreen-post-process-pass.ts +14 -57
  448. package/src/glyph-text-layout-system.ts +70 -96
  449. package/src/gpu-driven/batch-topology.ts +24 -202
  450. package/src/gpu-driven/production-raster.ts +99 -1109
  451. package/src/gpu-driven/view-gpu.ts +46 -192
  452. package/src/gpu-resource.ts +4 -4
  453. package/src/gpu-scene-schema.ts +11 -99
  454. package/src/gpu-scene.ts +231 -364
  455. package/src/hdrp-buffers.ts +12 -221
  456. package/src/ibl/IblPipelineCache.ts +471 -711
  457. package/src/ibl/skylight-bind-group.ts +4 -4
  458. package/src/index.ts +75 -217
  459. package/src/inspection-types.ts +80 -0
  460. package/src/instance-buffer-cache.ts +1 -29
  461. package/src/internal.ts +6 -85
  462. package/src/lifecycle.ts +64 -19
  463. package/src/materials.ts +7 -29
  464. package/src/mesh-material-bindings.ts +0 -1
  465. package/src/pbr-pipeline.ts +441 -53
  466. package/src/{urp-pipeline.ts → pipeline/standard-forward-lane.ts} +38 -149
  467. package/src/{hdrp-pipeline.ts → pipeline/standard-pipeline.ts} +221 -191
  468. package/src/pipeline/standard-profile.ts +65 -0
  469. package/src/pipeline-builder.ts +60 -64
  470. package/src/pipeline-spec-types.ts +28 -0
  471. package/src/pipeline-spec.ts +72 -609
  472. package/src/plugin.ts +7 -75
  473. package/src/points-lines/__tests__/bounds.unit.test.ts +21 -0
  474. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +157 -0
  475. package/src/points-lines/__tests__/inspection-lanes.integration.test.ts +146 -0
  476. package/src/points-lines/__tests__/inspection.unit.test.ts +173 -0
  477. package/src/points-lines/__tests__/lane-structural.integration.test.ts +122 -0
  478. package/src/points-lines/__tests__/points-lines.bench.ts +400 -0
  479. package/src/points-lines/__tests__/raster-lane.integration.test.ts +44 -0
  480. package/src/points-lines/__tests__/snapshot.unit.test.ts +101 -0
  481. package/src/points-lines/__tests__/view-ubo.unit.test.ts +73 -0
  482. package/src/points-lines/admission.ts +238 -0
  483. package/src/points-lines/bounds.ts +23 -0
  484. package/src/points-lines/expansion-cache.ts +215 -0
  485. package/src/points-lines/inspection.ts +97 -0
  486. package/src/points-lines/prepare.ts +177 -0
  487. package/src/points-lines/record.ts +125 -0
  488. package/src/points-lines/snapshot.ts +130 -0
  489. package/src/post-process-errors.ts +31 -33
  490. package/src/prepare/prepared-graphics-resolver.ts +5 -4
  491. package/src/record/__tests__/material-slot-plan.unit.test.ts +1 -33
  492. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +41 -0
  493. package/src/record/__tests__/video-texture-view.unit.test.ts +1 -1
  494. package/src/record/frame-lighting.ts +88 -197
  495. package/src/record/frame-snapshot.ts +45 -60
  496. package/src/record/frame-targets.ts +8 -243
  497. package/src/record/frame.ts +521 -797
  498. package/src/record/helpers.ts +6 -11
  499. package/src/record/main-pass-geometry.ts +198 -147
  500. package/src/record/main-pass-material.ts +517 -190
  501. package/src/record/main-pass-sprite-draws.ts +199 -118
  502. package/src/record/main-pass.ts +166 -144
  503. package/src/record/mesh-ssbo.ts +517 -38
  504. package/src/record/render-context.ts +384 -0
  505. package/src/record/shadow-pass.ts +154 -1843
  506. package/src/record/skybox-post-pass.ts +14 -15
  507. package/src/record/typed-frame-graph.ts +224 -262
  508. package/src/record/view-ubo.ts +53 -37
  509. package/src/render-contract.ts +582 -0
  510. package/src/render-data.ts +3 -3
  511. package/src/render-graph-primitives.ts +76 -851
  512. package/src/render-pipeline.ts +104 -188
  513. package/src/render-system-extract.ts +1114 -1732
  514. package/src/render-system.ts +614 -1939
  515. package/src/scene/render-scene-types.ts +67 -0
  516. package/src/scene/render-scene.ts +1087 -0
  517. package/src/scene-instances/post-spawn-resolve-joints.ts +4 -7
  518. package/src/ssao-buffers.ts +167 -8
  519. package/src/systems/skin-palette-allocator.ts +21 -64
  520. package/src/systems/skin-palette-types.ts +14 -0
  521. package/src/systems/transparent-sort-config.ts +25 -3
  522. package/src/tilemap-chunk-extract-system.ts +53 -70
  523. package/src/typed-render-graph-primitives.ts +35 -24
  524. package/src/typed-shadow-passes.ts +26 -56
  525. package/dist/__tests__/analytic-sky-contract.test.d.ts +0 -2
  526. package/dist/__tests__/analytic-sky-contract.test.d.ts.map +0 -1
  527. package/dist/__tests__/analytic-sky-falsify.test.d.ts +0 -2
  528. package/dist/__tests__/analytic-sky-falsify.test.d.ts.map +0 -1
  529. package/dist/__tests__/assets-runtime-handoff.integration.test.d.ts +0 -2
  530. package/dist/__tests__/assets-runtime-handoff.integration.test.d.ts.map +0 -1
  531. package/dist/__tests__/backend-selection.integration.test.d.ts +0 -2
  532. package/dist/__tests__/backend-selection.integration.test.d.ts.map +0 -1
  533. package/dist/__tests__/camera-temporal.test-d.d.ts +0 -2
  534. package/dist/__tests__/camera-temporal.test-d.d.ts.map +0 -1
  535. package/dist/__tests__/camera-temporal.test.d.ts +0 -2
  536. package/dist/__tests__/camera-temporal.test.d.ts.map +0 -1
  537. package/dist/__tests__/directional-shadow-probe-atlas-layout.unit.test.d.ts +0 -2
  538. package/dist/__tests__/directional-shadow-probe-atlas-layout.unit.test.d.ts.map +0 -1
  539. package/dist/__tests__/environment-inspection.test.d.ts +0 -2
  540. package/dist/__tests__/environment-inspection.test.d.ts.map +0 -1
  541. package/dist/__tests__/fog-ai-user.fixture.d.ts +0 -2
  542. package/dist/__tests__/fog-ai-user.fixture.d.ts.map +0 -1
  543. package/dist/__tests__/fog-custom-render-feature.fixture.d.ts +0 -16
  544. package/dist/__tests__/fog-custom-render-feature.fixture.d.ts.map +0 -1
  545. package/dist/__tests__/fog-custom-render-feature.integration.test.d.ts +0 -2
  546. package/dist/__tests__/fog-custom-render-feature.integration.test.d.ts.map +0 -1
  547. package/dist/__tests__/fog-inspection.integration.test.d.ts +0 -2
  548. package/dist/__tests__/fog-inspection.integration.test.d.ts.map +0 -1
  549. package/dist/__tests__/fog-owner-state.integration.test.d.ts +0 -2
  550. package/dist/__tests__/fog-owner-state.integration.test.d.ts.map +0 -1
  551. package/dist/__tests__/fog-pipeline-topology.integration.test.d.ts +0 -2
  552. package/dist/__tests__/fog-pipeline-topology.integration.test.d.ts.map +0 -1
  553. package/dist/__tests__/fog-public-surface.integration.test.d.ts +0 -2
  554. package/dist/__tests__/fog-public-surface.integration.test.d.ts.map +0 -1
  555. package/dist/__tests__/fog-ray-contract.integration.test.d.ts +0 -2
  556. package/dist/__tests__/fog-ray-contract.integration.test.d.ts.map +0 -1
  557. package/dist/__tests__/fog-source-absence.integration.test.d.ts +0 -2
  558. package/dist/__tests__/fog-source-absence.integration.test.d.ts.map +0 -1
  559. package/dist/__tests__/fog-temporal.integration.test.d.ts +0 -2
  560. package/dist/__tests__/fog-temporal.integration.test.d.ts.map +0 -1
  561. package/dist/__tests__/frame-chain.unit.test.d.ts +0 -2
  562. package/dist/__tests__/frame-chain.unit.test.d.ts.map +0 -1
  563. package/dist/__tests__/gpu-scene-field-type-owner.test-d.d.ts +0 -2
  564. package/dist/__tests__/gpu-scene-field-type-owner.test-d.d.ts.map +0 -1
  565. package/dist/__tests__/hdrp-pipeline.unit.test.d.ts +0 -2
  566. package/dist/__tests__/hdrp-pipeline.unit.test.d.ts.map +0 -1
  567. package/dist/__tests__/ibl/ibl-fallback.unit.test.d.ts +0 -2
  568. package/dist/__tests__/ibl/ibl-fallback.unit.test.d.ts.map +0 -1
  569. package/dist/__tests__/material-derived-error-stop-draw.integration.test.d.ts +0 -2
  570. package/dist/__tests__/material-derived-error-stop-draw.integration.test.d.ts.map +0 -1
  571. package/dist/__tests__/material-derived-payload.integration.test.d.ts +0 -2
  572. package/dist/__tests__/material-derived-payload.integration.test.d.ts.map +0 -1
  573. package/dist/__tests__/materials.unit.test.d.ts +0 -2
  574. package/dist/__tests__/materials.unit.test.d.ts.map +0 -1
  575. package/dist/__tests__/param-schema-projection-census.unit.test.d.ts +0 -2
  576. package/dist/__tests__/param-schema-projection-census.unit.test.d.ts.map +0 -1
  577. package/dist/__tests__/persistent-render-scene.unit.test.d.ts +0 -2
  578. package/dist/__tests__/persistent-render-scene.unit.test.d.ts.map +0 -1
  579. package/dist/__tests__/pipeline-error-policy-owner.unit.test.d.ts +0 -2
  580. package/dist/__tests__/pipeline-error-policy-owner.unit.test.d.ts.map +0 -1
  581. package/dist/__tests__/pipeline-pass-trace.test.d.ts +0 -2
  582. package/dist/__tests__/pipeline-pass-trace.test.d.ts.map +0 -1
  583. package/dist/__tests__/pipeline-stage-owner.test-d.d.ts +0 -2
  584. package/dist/__tests__/pipeline-stage-owner.test-d.d.ts.map +0 -1
  585. package/dist/__tests__/plugin.test.d.ts +0 -2
  586. package/dist/__tests__/plugin.test.d.ts.map +0 -1
  587. package/dist/__tests__/prepared-graphics-backend-matrix.unit.test.d.ts +0 -2
  588. package/dist/__tests__/prepared-graphics-backend-matrix.unit.test.d.ts.map +0 -1
  589. package/dist/__tests__/prepared-graphics-resolver-recording.unit.test.d.ts +0 -2
  590. package/dist/__tests__/prepared-graphics-resolver-recording.unit.test.d.ts.map +0 -1
  591. package/dist/__tests__/public-pipeline-id.test-d.d.ts +0 -2
  592. package/dist/__tests__/public-pipeline-id.test-d.d.ts.map +0 -1
  593. package/dist/__tests__/render-dispatch-membership.unit.test.d.ts +0 -2
  594. package/dist/__tests__/render-dispatch-membership.unit.test.d.ts.map +0 -1
  595. package/dist/__tests__/render-feature-construction.unit.test.d.ts +0 -2
  596. package/dist/__tests__/render-feature-construction.unit.test.d.ts.map +0 -1
  597. package/dist/__tests__/render-feature-error-code-owner.test-d.d.ts +0 -2
  598. package/dist/__tests__/render-feature-error-code-owner.test-d.d.ts.map +0 -1
  599. package/dist/__tests__/render-feature-gpu-buffer-usage-owner.test-d.d.ts +0 -2
  600. package/dist/__tests__/render-feature-gpu-buffer-usage-owner.test-d.d.ts.map +0 -1
  601. package/dist/__tests__/render-feature-graph.integration.test.d.ts +0 -2
  602. package/dist/__tests__/render-feature-graph.integration.test.d.ts.map +0 -1
  603. package/dist/__tests__/render-feature-graphics-staging.unit.test.d.ts +0 -2
  604. package/dist/__tests__/render-feature-graphics-staging.unit.test.d.ts.map +0 -1
  605. package/dist/__tests__/render-feature-order.unit.test.d.ts +0 -2
  606. package/dist/__tests__/render-feature-order.unit.test.d.ts.map +0 -1
  607. package/dist/__tests__/render-feature-prepared-graphics-recording.unit.test.d.ts +0 -2
  608. package/dist/__tests__/render-feature-prepared-graphics-recording.unit.test.d.ts.map +0 -1
  609. package/dist/__tests__/render-feature-staging.unit.test.d.ts +0 -2
  610. package/dist/__tests__/render-feature-staging.unit.test.d.ts.map +0 -1
  611. package/dist/__tests__/renderer-render-scene-public.test-d.d.ts +0 -2
  612. package/dist/__tests__/renderer-render-scene-public.test-d.d.ts.map +0 -1
  613. package/dist/__tests__/scene-temporal-coverage.unit.test.d.ts +0 -2
  614. package/dist/__tests__/scene-temporal-coverage.unit.test.d.ts.map +0 -1
  615. package/dist/__tests__/sun-snapshot-consumers.test.d.ts +0 -2
  616. package/dist/__tests__/sun-snapshot-consumers.test.d.ts.map +0 -1
  617. package/dist/__tests__/surface-backend-kind-owner.test-d.d.ts +0 -2
  618. package/dist/__tests__/surface-backend-kind-owner.test-d.d.ts.map +0 -1
  619. package/dist/__tests__/taa-history-store.unit.test.d.ts +0 -2
  620. package/dist/__tests__/taa-history-store.unit.test.d.ts.map +0 -1
  621. package/dist/__tests__/temporal-frame-transaction.integration.test.d.ts +0 -2
  622. package/dist/__tests__/temporal-frame-transaction.integration.test.d.ts.map +0 -1
  623. package/dist/__tests__/temporal-inspection.unit.test.d.ts +0 -2
  624. package/dist/__tests__/temporal-inspection.unit.test.d.ts.map +0 -1
  625. package/dist/assets/__tests__/asset-decoders.unit.test.d.ts +0 -2
  626. package/dist/assets/__tests__/asset-decoders.unit.test.d.ts.map +0 -1
  627. package/dist/assets/__tests__/mipmap-generator.unit.test.d.ts +0 -2
  628. package/dist/assets/__tests__/mipmap-generator.unit.test.d.ts.map +0 -1
  629. package/dist/assets/__tests__/render-asset-port.unit.test.d.ts +0 -2
  630. package/dist/assets/__tests__/render-asset-port.unit.test.d.ts.map +0 -1
  631. package/dist/assets/__tests__/resolve-render-asset.unit.test.d.ts +0 -2
  632. package/dist/assets/__tests__/resolve-render-asset.unit.test.d.ts.map +0 -1
  633. package/dist/chunk-DJIMFYX6.mjs +0 -1439
  634. package/dist/chunk-DJIMFYX6.mjs.map +0 -1
  635. package/dist/chunk-F2SIOCRM.mjs +0 -7423
  636. package/dist/chunk-F2SIOCRM.mjs.map +0 -1
  637. package/dist/chunk-JMSB7AR4.mjs +0 -3
  638. package/dist/chunk-JMSB7AR4.mjs.map +0 -1
  639. package/dist/chunk-KHI6K4C6.mjs +0 -659
  640. package/dist/chunk-KHI6K4C6.mjs.map +0 -1
  641. package/dist/chunk-PORZOL2V.mjs +0 -569
  642. package/dist/chunk-PORZOL2V.mjs.map +0 -1
  643. package/dist/chunk-UOJGMDGP.mjs +0 -29913
  644. package/dist/chunk-UOJGMDGP.mjs.map +0 -1
  645. package/dist/components/__tests__/atmosphere-red.test.d.ts +0 -2
  646. package/dist/components/__tests__/atmosphere-red.test.d.ts.map +0 -1
  647. package/dist/components/__tests__/atmosphere.test.d.ts +0 -2
  648. package/dist/components/__tests__/atmosphere.test.d.ts.map +0 -1
  649. package/dist/components/__tests__/fog.test-d.d.ts +0 -2
  650. package/dist/components/__tests__/fog.test-d.d.ts.map +0 -1
  651. package/dist/components/__tests__/fog.test.d.ts +0 -2
  652. package/dist/components/__tests__/fog.test.d.ts.map +0 -1
  653. package/dist/components/atmosphere.d.ts +0 -18
  654. package/dist/components/atmosphere.d.ts.map +0 -1
  655. package/dist/components/environment.d.ts +0 -115
  656. package/dist/components/environment.d.ts.map +0 -1
  657. package/dist/deferred-lighting.d.ts +0 -13
  658. package/dist/deferred-lighting.d.ts.map +0 -1
  659. package/dist/dynamic-texture-store.d.ts +0 -42
  660. package/dist/dynamic-texture-store.d.ts.map +0 -1
  661. package/dist/environment/__tests__/failure-injection.test.d.ts +0 -2
  662. package/dist/environment/__tests__/failure-injection.test.d.ts.map +0 -1
  663. package/dist/environment/__tests__/generation-red.test.d.ts +0 -2
  664. package/dist/environment/__tests__/generation-red.test.d.ts.map +0 -1
  665. package/dist/environment/__tests__/generation.test.d.ts +0 -2
  666. package/dist/environment/__tests__/generation.test.d.ts.map +0 -1
  667. package/dist/environment/__tests__/graph-transaction.test.d.ts +0 -2
  668. package/dist/environment/__tests__/graph-transaction.test.d.ts.map +0 -1
  669. package/dist/environment/__tests__/inspection-red.test.d.ts +0 -2
  670. package/dist/environment/__tests__/inspection-red.test.d.ts.map +0 -1
  671. package/dist/environment/__tests__/lifecycle-evidence.test.d.ts +0 -2
  672. package/dist/environment/__tests__/lifecycle-evidence.test.d.ts.map +0 -1
  673. package/dist/environment/__tests__/pipeline-topology.test.d.ts +0 -2
  674. package/dist/environment/__tests__/pipeline-topology.test.d.ts.map +0 -1
  675. package/dist/environment/__tests__/readiness.test.d.ts +0 -2
  676. package/dist/environment/__tests__/readiness.test.d.ts.map +0 -1
  677. package/dist/environment/__tests__/recovery.test.d.ts +0 -2
  678. package/dist/environment/__tests__/recovery.test.d.ts.map +0 -1
  679. package/dist/environment/__tests__/resize-dispose.test.d.ts +0 -2
  680. package/dist/environment/__tests__/resize-dispose.test.d.ts.map +0 -1
  681. package/dist/environment/__tests__/signature-red.test.d.ts +0 -2
  682. package/dist/environment/__tests__/signature-red.test.d.ts.map +0 -1
  683. package/dist/environment/__tests__/signature.test.d.ts +0 -2
  684. package/dist/environment/__tests__/signature.test.d.ts.map +0 -1
  685. package/dist/environment/descriptors.d.ts +0 -2
  686. package/dist/environment/descriptors.d.ts.map +0 -1
  687. package/dist/environment/extracted-environment.d.ts +0 -53
  688. package/dist/environment/extracted-environment.d.ts.map +0 -1
  689. package/dist/environment/generation.d.ts +0 -62
  690. package/dist/environment/generation.d.ts.map +0 -1
  691. package/dist/environment/gpu-bundle.d.ts +0 -23
  692. package/dist/environment/gpu-bundle.d.ts.map +0 -1
  693. package/dist/environment/graph.d.ts +0 -108
  694. package/dist/environment/graph.d.ts.map +0 -1
  695. package/dist/environment/inspection.d.ts +0 -48
  696. package/dist/environment/inspection.d.ts.map +0 -1
  697. package/dist/environment/lifecycle.d.ts +0 -22
  698. package/dist/environment/lifecycle.d.ts.map +0 -1
  699. package/dist/environment/readiness.d.ts +0 -29
  700. package/dist/environment/readiness.d.ts.map +0 -1
  701. package/dist/environment/signature.d.ts +0 -70
  702. package/dist/environment/signature.d.ts.map +0 -1
  703. package/dist/extract/__tests__/directional-csm-domain.unit.test.d.ts +0 -2
  704. package/dist/extract/__tests__/directional-csm-domain.unit.test.d.ts.map +0 -1
  705. package/dist/extract/__tests__/environment-source.test.d.ts +0 -2
  706. package/dist/extract/__tests__/environment-source.test.d.ts.map +0 -1
  707. package/dist/extract/__tests__/schema-readers.unit.test.d.ts +0 -2
  708. package/dist/extract/__tests__/schema-readers.unit.test.d.ts.map +0 -1
  709. package/dist/extract/directional-csm.d.ts +0 -129
  710. package/dist/extract/directional-csm.d.ts.map +0 -1
  711. package/dist/extract/index.d.ts +0 -5
  712. package/dist/extract/index.d.ts.map +0 -1
  713. package/dist/extract/material-interface.d.ts +0 -56
  714. package/dist/extract/material-interface.d.ts.map +0 -1
  715. package/dist/extract/schema-readers.d.ts +0 -20
  716. package/dist/extract/schema-readers.d.ts.map +0 -1
  717. package/dist/features/graph-contribution.d.ts +0 -64
  718. package/dist/features/graph-contribution.d.ts.map +0 -1
  719. package/dist/features/index.d.ts +0 -21
  720. package/dist/features/index.d.ts.map +0 -1
  721. package/dist/features/morph/index.d.ts +0 -3
  722. package/dist/features/morph/index.d.ts.map +0 -1
  723. package/dist/features/morph/morph-bounds.d.ts +0 -42
  724. package/dist/features/morph/morph-bounds.d.ts.map +0 -1
  725. package/dist/features/scene-temporal.d.ts +0 -45
  726. package/dist/features/scene-temporal.d.ts.map +0 -1
  727. package/dist/features/taa-resolve.d.ts +0 -21
  728. package/dist/features/taa-resolve.d.ts.map +0 -1
  729. package/dist/frame-chain.d.ts +0 -14
  730. package/dist/frame-chain.d.ts.map +0 -1
  731. package/dist/gpu-driven/view-reference.d.ts.map +0 -1
  732. package/dist/gpu-resource-store.d.ts.map +0 -1
  733. package/dist/hdrp-cluster-membership.d.ts +0 -2
  734. package/dist/hdrp-cluster-membership.d.ts.map +0 -1
  735. package/dist/hdrp-pipeline.d.ts +0 -36
  736. package/dist/hdrp-pipeline.d.ts.map +0 -1
  737. package/dist/ibl/cubemap-projection.d.ts +0 -23
  738. package/dist/ibl/cubemap-projection.d.ts.map +0 -1
  739. package/dist/ibl/face-uniforms.d.ts +0 -65
  740. package/dist/ibl/face-uniforms.d.ts.map +0 -1
  741. package/dist/mesh-update-errors.d.ts +0 -71
  742. package/dist/mesh-update-errors.d.ts.map +0 -1
  743. package/dist/mipmap-generator.d.ts +0 -39
  744. package/dist/mipmap-generator.d.ts.map +0 -1
  745. package/dist/param-schema-projection-census.d.ts +0 -18
  746. package/dist/param-schema-projection-census.d.ts.map +0 -1
  747. package/dist/persistent-render-scene.d.ts +0 -158
  748. package/dist/persistent-render-scene.d.ts.map +0 -1
  749. package/dist/pipeline/index.d.ts +0 -11
  750. package/dist/pipeline/index.d.ts.map +0 -1
  751. package/dist/pipeline-errors.d.ts +0 -110
  752. package/dist/pipeline-errors.d.ts.map +0 -1
  753. package/dist/prepare/__tests__/schema-readers.unit.test.d.ts +0 -2
  754. package/dist/prepare/__tests__/schema-readers.unit.test.d.ts.map +0 -1
  755. package/dist/prepare/index.d.ts +0 -4
  756. package/dist/prepare/index.d.ts.map +0 -1
  757. package/dist/prepare/schema-readers.d.ts +0 -11
  758. package/dist/prepare/schema-readers.d.ts.map +0 -1
  759. package/dist/record/__tests__/environment-record.test.d.ts +0 -2
  760. package/dist/record/__tests__/environment-record.test.d.ts.map +0 -1
  761. package/dist/record/__tests__/frame-targets.unit.test.d.ts +0 -2
  762. package/dist/record/__tests__/frame-targets.unit.test.d.ts.map +0 -1
  763. package/dist/record/__tests__/membership-timing.unit.test.d.ts +0 -2
  764. package/dist/record/__tests__/membership-timing.unit.test.d.ts.map +0 -1
  765. package/dist/record/__tests__/render-submission.integration.test.d.ts +0 -2
  766. package/dist/record/__tests__/render-submission.integration.test.d.ts.map +0 -1
  767. package/dist/record/__tests__/render-submission.perf.test.d.ts +0 -2
  768. package/dist/record/__tests__/render-submission.perf.test.d.ts.map +0 -1
  769. package/dist/record/__tests__/shadow-submit-mode.unit.test.d.ts +0 -2
  770. package/dist/record/__tests__/shadow-submit-mode.unit.test.d.ts.map +0 -1
  771. package/dist/record/__tests__/view-ubo-layout.unit.test.d.ts +0 -2
  772. package/dist/record/__tests__/view-ubo-layout.unit.test.d.ts.map +0 -1
  773. package/dist/record/frame-observation.d.ts +0 -38
  774. package/dist/record/frame-observation.d.ts.map +0 -1
  775. package/dist/record/index.d.ts +0 -15
  776. package/dist/record/index.d.ts.map +0 -1
  777. package/dist/record/main-pass-skin.d.ts +0 -46
  778. package/dist/record/main-pass-skin.d.ts.map +0 -1
  779. package/dist/record/main-pass-sprite.d.ts +0 -69
  780. package/dist/record/main-pass-sprite.d.ts.map +0 -1
  781. package/dist/record/material-payload.d.ts +0 -43
  782. package/dist/record/material-payload.d.ts.map +0 -1
  783. package/dist/record/material-slot-plan.d.ts +0 -26
  784. package/dist/record/material-slot-plan.d.ts.map +0 -1
  785. package/dist/record/membership-timing.d.ts +0 -232
  786. package/dist/record/membership-timing.d.ts.map +0 -1
  787. package/dist/record/shadow-caster-uniform.d.ts +0 -8
  788. package/dist/record/shadow-caster-uniform.d.ts.map +0 -1
  789. package/dist/record/shadow-submit-mode.d.ts +0 -66
  790. package/dist/record/shadow-submit-mode.d.ts.map +0 -1
  791. package/dist/render-dispatch-membership.d.ts +0 -47
  792. package/dist/render-dispatch-membership.d.ts.map +0 -1
  793. package/dist/render-pipeline-context.d.ts +0 -289
  794. package/dist/render-pipeline-context.d.ts.map +0 -1
  795. package/dist/render-pipeline-target.d.ts +0 -16
  796. package/dist/render-pipeline-target.d.ts.map +0 -1
  797. package/dist/render-scene-projection.d.ts +0 -174
  798. package/dist/render-scene-projection.d.ts.map +0 -1
  799. package/dist/render-system-fold.d.ts +0 -266
  800. package/dist/render-system-fold.d.ts.map +0 -1
  801. package/dist/renderer/asset-service.d.ts +0 -10
  802. package/dist/renderer/asset-service.d.ts.map +0 -1
  803. package/dist/renderer/backend-selection.d.ts.map +0 -1
  804. package/dist/renderer/environment-classify.d.ts +0 -6
  805. package/dist/renderer/environment-classify.d.ts.map +0 -1
  806. package/dist/renderer/environment-error.d.ts +0 -13
  807. package/dist/renderer/environment-error.d.ts.map +0 -1
  808. package/dist/renderer/material/pipeline-projection.d.ts +0 -91
  809. package/dist/renderer/material/pipeline-projection.d.ts.map +0 -1
  810. package/dist/renderer/renderer-factory.d.ts +0 -289
  811. package/dist/renderer/renderer-factory.d.ts.map +0 -1
  812. package/dist/renderer.d.ts +0 -771
  813. package/dist/renderer.d.ts.map +0 -1
  814. package/dist/scene-utils/collect-subtree.d.ts +0 -11
  815. package/dist/scene-utils/collect-subtree.d.ts.map +0 -1
  816. package/dist/ssao-data.d.ts +0 -34
  817. package/dist/ssao-data.d.ts.map +0 -1
  818. package/dist/surface-format.d.ts +0 -10
  819. package/dist/surface-format.d.ts.map +0 -1
  820. package/dist/taa-history-store.d.ts +0 -88
  821. package/dist/taa-history-store.d.ts.map +0 -1
  822. package/dist/temporal-coverage.d.ts +0 -35
  823. package/dist/temporal-coverage.d.ts.map +0 -1
  824. package/dist/temporal-frame-transaction.d.ts +0 -92
  825. package/dist/temporal-frame-transaction.d.ts.map +0 -1
  826. package/dist/temporal-inspection.d.ts +0 -57
  827. package/dist/temporal-inspection.d.ts.map +0 -1
  828. package/dist/temporal-view.d.ts +0 -43
  829. package/dist/temporal-view.d.ts.map +0 -1
  830. package/dist/tilemap/resolve-tileset-runtime.d.ts +0 -23
  831. package/dist/tilemap/resolve-tileset-runtime.d.ts.map +0 -1
  832. package/dist/urp-pipeline.d.ts +0 -4
  833. package/dist/urp-pipeline.d.ts.map +0 -1
  834. package/src/__tests__/analytic-sky-contract.test.ts +0 -27
  835. package/src/__tests__/analytic-sky-falsify-carriers.mjs +0 -43
  836. package/src/__tests__/analytic-sky-falsify.test.ts +0 -94
  837. package/src/__tests__/assets-runtime-handoff.integration.test.ts +0 -36
  838. package/src/__tests__/backend-selection.integration.test.ts +0 -11
  839. package/src/__tests__/camera-temporal.test-d.ts +0 -8
  840. package/src/__tests__/camera-temporal.test.ts +0 -188
  841. package/src/__tests__/directional-shadow-probe-atlas-layout.unit.test.ts +0 -18
  842. package/src/__tests__/environment-inspection.test.ts +0 -45
  843. package/src/__tests__/fog-ai-user.fixture.ts +0 -350
  844. package/src/__tests__/fog-custom-render-feature.fixture.ts +0 -33
  845. package/src/__tests__/fog-custom-render-feature.integration.test.ts +0 -76
  846. package/src/__tests__/fog-inspection.integration.test.ts +0 -127
  847. package/src/__tests__/fog-owner-state.integration.test.ts +0 -112
  848. package/src/__tests__/fog-pipeline-topology.integration.test.ts +0 -93
  849. package/src/__tests__/fog-public-surface.integration.test.ts +0 -60
  850. package/src/__tests__/fog-ray-contract.integration.test.ts +0 -73
  851. package/src/__tests__/fog-source-absence.integration.test.ts +0 -51
  852. package/src/__tests__/fog-temporal.integration.test.ts +0 -115
  853. package/src/__tests__/frame-chain.unit.test.ts +0 -26
  854. package/src/__tests__/gpu-scene-field-type-owner.test-d.ts +0 -27
  855. package/src/__tests__/hdrp-pipeline.unit.test.ts +0 -100
  856. package/src/__tests__/ibl/ibl-fallback.unit.test.ts +0 -23
  857. package/src/__tests__/material-derived-error-stop-draw.integration.test.ts +0 -69
  858. package/src/__tests__/material-derived-payload.integration.test.ts +0 -164
  859. package/src/__tests__/materials.unit.test.ts +0 -25
  860. package/src/__tests__/param-schema-projection-census.unit.test.ts +0 -40
  861. package/src/__tests__/persistent-render-scene.unit.test.ts +0 -1311
  862. package/src/__tests__/pipeline-error-policy-owner.unit.test.ts +0 -167
  863. package/src/__tests__/pipeline-pass-trace.test.ts +0 -92
  864. package/src/__tests__/pipeline-stage-owner.test-d.ts +0 -13
  865. package/src/__tests__/plugin.test.ts +0 -25
  866. package/src/__tests__/prepared-graphics-backend-matrix.unit.test.ts +0 -129
  867. package/src/__tests__/prepared-graphics-resolver-recording.unit.test.ts +0 -180
  868. package/src/__tests__/public-pipeline-id.test-d.ts +0 -6
  869. package/src/__tests__/render-dispatch-membership.unit.test.ts +0 -117
  870. package/src/__tests__/render-feature-construction.unit.test.ts +0 -124
  871. package/src/__tests__/render-feature-error-code-owner.test-d.ts +0 -27
  872. package/src/__tests__/render-feature-gpu-buffer-usage-owner.test-d.ts +0 -46
  873. package/src/__tests__/render-feature-graph.integration.test.ts +0 -205
  874. package/src/__tests__/render-feature-graphics-staging.unit.test.ts +0 -200
  875. package/src/__tests__/render-feature-order.unit.test.ts +0 -66
  876. package/src/__tests__/render-feature-prepared-graphics-recording.unit.test.ts +0 -107
  877. package/src/__tests__/render-feature-staging.unit.test.ts +0 -94
  878. package/src/__tests__/renderer-render-scene-public.test-d.ts +0 -27
  879. package/src/__tests__/scene-temporal-coverage.unit.test.ts +0 -259
  880. package/src/__tests__/sun-snapshot-consumers.test.ts +0 -35
  881. package/src/__tests__/surface-backend-kind-owner.test-d.ts +0 -42
  882. package/src/__tests__/taa-history-store.unit.test.ts +0 -196
  883. package/src/__tests__/temporal-frame-transaction.integration.test.ts +0 -537
  884. package/src/__tests__/temporal-inspection.unit.test.ts +0 -138
  885. package/src/assets/__tests__/asset-decoders.unit.test.ts +0 -92
  886. package/src/assets/__tests__/mipmap-generator.unit.test.ts +0 -52
  887. package/src/assets/__tests__/render-asset-port.unit.test.ts +0 -54
  888. package/src/assets/__tests__/resolve-render-asset.unit.test.ts +0 -47
  889. package/src/components/__tests__/atmosphere-red.test.ts +0 -51
  890. package/src/components/__tests__/atmosphere.test.ts +0 -79
  891. package/src/components/__tests__/fog.test-d.ts +0 -49
  892. package/src/components/__tests__/fog.test.ts +0 -60
  893. package/src/components/atmosphere.ts +0 -64
  894. package/src/deferred-lighting.ts +0 -92
  895. package/src/dynamic-texture-store.ts +0 -111
  896. package/src/environment/README.md +0 -76
  897. package/src/environment/__tests__/failure-injection.test.ts +0 -50
  898. package/src/environment/__tests__/generation-red.test.ts +0 -64
  899. package/src/environment/__tests__/generation.test.ts +0 -37
  900. package/src/environment/__tests__/graph-transaction.test.ts +0 -85
  901. package/src/environment/__tests__/inspection-red.test.ts +0 -53
  902. package/src/environment/__tests__/lifecycle-evidence.test.ts +0 -40
  903. package/src/environment/__tests__/pipeline-topology.test.ts +0 -33
  904. package/src/environment/__tests__/readiness.test.ts +0 -42
  905. package/src/environment/__tests__/recovery.test.ts +0 -62
  906. package/src/environment/__tests__/resize-dispose.test.ts +0 -30
  907. package/src/environment/__tests__/signature-red.test.ts +0 -45
  908. package/src/environment/__tests__/signature.test.ts +0 -60
  909. package/src/environment/descriptors.ts +0 -7
  910. package/src/environment/extracted-environment.ts +0 -239
  911. package/src/environment/generation.ts +0 -158
  912. package/src/environment/gpu-bundle.ts +0 -144
  913. package/src/environment/graph.ts +0 -588
  914. package/src/environment/inspection.ts +0 -107
  915. package/src/environment/lifecycle.ts +0 -124
  916. package/src/environment/readiness.ts +0 -76
  917. package/src/environment/signature.ts +0 -185
  918. package/src/extract/__tests__/directional-csm-domain.unit.test.ts +0 -391
  919. package/src/extract/__tests__/environment-source.test.ts +0 -105
  920. package/src/extract/__tests__/schema-readers.unit.test.ts +0 -16
  921. package/src/extract/directional-csm.ts +0 -959
  922. package/src/extract/index.ts +0 -10
  923. package/src/extract/material-interface.ts +0 -604
  924. package/src/extract/schema-readers.ts +0 -27
  925. package/src/features/graph-contribution.ts +0 -577
  926. package/src/features/index.ts +0 -100
  927. package/src/features/morph/index.ts +0 -24
  928. package/src/features/morph/morph-bounds.ts +0 -123
  929. package/src/features/scene-temporal.ts +0 -274
  930. package/src/features/taa-resolve.ts +0 -244
  931. package/src/frame-chain.ts +0 -21
  932. package/src/hdrp-cluster-membership.ts +0 -70
  933. package/src/ibl/cubemap-projection.ts +0 -60
  934. package/src/ibl/face-uniforms.ts +0 -150
  935. package/src/mesh-update-errors.ts +0 -113
  936. package/src/mipmap-generator.ts +0 -244
  937. package/src/param-schema-projection-census.ts +0 -73
  938. package/src/persistent-render-scene.ts +0 -1752
  939. package/src/pipeline/index.ts +0 -40
  940. package/src/pipeline-errors.ts +0 -172
  941. package/src/prepare/__tests__/schema-readers.unit.test.ts +0 -16
  942. package/src/prepare/index.ts +0 -8
  943. package/src/prepare/schema-readers.ts +0 -20
  944. package/src/record/__tests__/environment-record.test.ts +0 -35
  945. package/src/record/__tests__/frame-targets.unit.test.ts +0 -54
  946. package/src/record/__tests__/membership-timing.unit.test.ts +0 -524
  947. package/src/record/__tests__/render-submission.integration.test.ts +0 -76
  948. package/src/record/__tests__/render-submission.perf.test.ts +0 -141
  949. package/src/record/__tests__/shadow-submit-mode.unit.test.ts +0 -88
  950. package/src/record/__tests__/view-ubo-layout.unit.test.ts +0 -101
  951. package/src/record/frame-observation.ts +0 -122
  952. package/src/record/index.ts +0 -15
  953. package/src/record/main-pass-skin.ts +0 -51
  954. package/src/record/main-pass-sprite.ts +0 -166
  955. package/src/record/material-payload.ts +0 -292
  956. package/src/record/material-slot-plan.ts +0 -68
  957. package/src/record/membership-timing.ts +0 -1012
  958. package/src/record/shadow-caster-uniform.ts +0 -37
  959. package/src/record/shadow-submit-mode.ts +0 -277
  960. package/src/render-dispatch-membership.ts +0 -155
  961. package/src/render-pipeline-context.ts +0 -332
  962. package/src/render-pipeline-target.ts +0 -96
  963. package/src/render-scene-projection.ts +0 -1738
  964. package/src/render-system-fold.ts +0 -531
  965. package/src/renderer/asset-service.ts +0 -32
  966. package/src/renderer/backend-selection.ts +0 -121
  967. package/src/renderer/environment-classify.ts +0 -30
  968. package/src/renderer/environment-error.ts +0 -19
  969. package/src/renderer/material/pipeline-projection.ts +0 -140
  970. package/src/renderer.ts +0 -940
  971. package/src/scene-utils/collect-subtree.ts +0 -40
  972. package/src/ssao-data.ts +0 -106
  973. package/src/surface-format.ts +0 -37
  974. package/src/taa-history-store.ts +0 -429
  975. package/src/temporal-coverage.ts +0 -106
  976. package/src/temporal-frame-transaction.ts +0 -301
  977. package/src/temporal-inspection.ts +0 -194
  978. package/src/temporal-view.ts +0 -174
  979. package/src/tilemap/resolve-tileset-runtime.ts +0 -72
@@ -4,9 +4,9 @@
4
4
  // arrays consumed by the Record stage.
5
5
  //
6
6
  // Carve-out from render-system.ts (review round 1 finding #3 - 505 line cap
7
- // fallback split into 3 files; main + extract + record). Public API
8
- // unchanged for engine-owned consumers: RenderSystem lives at
9
- // '@forgeax/engine-render/internal'.
7
+ // fallback split into 3 files; main + extract + record). The public API remains
8
+ // limited to the renderer contract; this module is an owner-local
9
+ // implementation seam consumed through relative imports.
10
10
  //
11
11
  // w15 (feat-20260511-asset-system-v1 M5 / plan-strategy D-P4) +
12
12
  // feat-20260513-component-naming-bevy-align M3 / D-2 (merged) +
@@ -77,7 +77,7 @@
77
77
  //
78
78
  // feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15:
79
79
  // The legacy `Instances { buffer, count }` form (cross-coupled with the
80
- // deleted `RenderAssetPort.createInstancedBuffer` triplet + the retired
80
+ // deleted `AssetRegistry.createInstancedBuffer` triplet + the retired
81
81
  // `InstancedBufferAsset` POD) is gone. Per-entity instance transforms now
82
82
  // live inside the ECS `Instances { transforms: 'array<f32>' }` column;
83
83
  // extract materialises a `Float32Array` snapshot per Instances-bearing
@@ -97,16 +97,23 @@
97
97
  // Layer-3 ErrorHandler and the renderable is skipped. The record stage
98
98
  // trusts the invariant and does not re-check.
99
99
 
100
- import type { EntityHandle, World } from '@forgeax/engine-ecs';
100
+ import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
101
+ import {
102
+ resolveAssetHandle,
103
+ walkMaterialPassesOverSharedRefs,
104
+ } from '@forgeax/engine-assets-runtime';
105
+ import type { Component, ComponentSchema, EntityHandle, World } from '@forgeax/engine-ecs';
101
106
  import {
102
107
  InstanceTransformsStrideMismatchError,
108
+ readRenderArrayView,
103
109
  routeWorldError,
110
+ SpawnLightInvalidBoundsError,
104
111
  SpriteInstancesCountMismatchError,
105
112
  SpriteInstancesMutuallyExclusiveWithInstancesError,
106
113
  SpriteInstancesRequiresSpriteShaderError,
107
114
  } from '@forgeax/engine-ecs/projection';
108
115
  import { box3, frustum, type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
109
- import { AssetGuid } from '@forgeax/engine-pack/guid';
116
+ import { AssetGuid, type AssetGuid as AssetGuidBytes } from '@forgeax/engine-pack/guid';
110
117
  import { RhiError } from '@forgeax/engine-rhi';
111
118
  import {
112
119
  MorphWeights,
@@ -114,7 +121,6 @@ import {
114
121
  type SceneHierarchySnapshot,
115
122
  Transform,
116
123
  } from '@forgeax/engine-scene';
117
- import { DEFAULT_UNLIT_PARAM_SCHEMA } from '@forgeax/engine-shader';
118
124
  import {
119
125
  JointCountMismatchError,
120
126
  JointEntityDanglingError,
@@ -124,11 +130,9 @@ import {
124
130
  } from '@forgeax/engine-skinning';
125
131
  import type {
126
132
  Asset,
127
- DerivedMaterialInterface,
128
133
  Handle,
129
134
  MaterialAsset,
130
135
  MaterialColorParameterSchema,
131
- MaterialError,
132
136
  MaterialParameter,
133
137
  MaterialPass,
134
138
  MaterialRenderState,
@@ -138,369 +142,66 @@ import type {
138
142
  ParamSchemaEntry,
139
143
  PrimitiveTopology,
140
144
  SkeletonAsset,
141
- TextureAsset,
142
145
  } from '@forgeax/engine-types';
143
146
  import {
144
147
  ASSET_ERROR_HINTS,
145
148
  AssetError,
146
149
  derive,
147
- err,
148
150
  materialGuidText,
149
151
  materialValuesToLinearRuntime,
150
- ok,
151
- RenderQueue,
152
- type Result,
153
152
  toShared,
154
153
  } from '@forgeax/engine-types';
155
- import type { RenderAssetPort } from './assets/render-asset-port';
156
- import { resolveRenderAsset } from './assets/resolve-render-asset';
157
-
158
- function reportRenderError(error: unknown, _context?: unknown): never {
159
- throw error;
160
- }
161
-
162
154
  import {
163
- type Antialias,
164
155
  antialiasFromF32,
165
- type BloomEnabled,
166
156
  bloomEnabledFromF32,
167
157
  Camera,
168
158
  cameraProjectionFromF32,
169
159
  DirectionalLight,
170
- Fog,
171
160
  Instances,
172
161
  Layer,
162
+ Lines,
173
163
  MeshFilter,
174
164
  MeshRenderer,
175
165
  PointLight,
176
166
  PointLightShadow,
167
+ Points,
177
168
  PostProcessParams,
169
+ pointShapeFromU32,
178
170
  SkyboxBackground,
179
171
  Skylight,
172
+ SortKey,
180
173
  SpotLight,
181
174
  SpriteInstances,
182
175
  SpriteRegionOverride,
183
- type Tonemap,
184
176
  tonemapFromF32,
185
177
  tonemapToU32,
186
178
  } from './components';
187
179
  import { GlyphText } from './components/glyph-text';
180
+ import { computeInvRangeSquared, degToCos } from './components/light-helpers';
188
181
  import {
189
- computeInvRangeSquared,
190
- degToCos,
191
- validateDirectionalLightData,
192
- validatePointLightData,
193
- validatePointLightShadowData,
194
- validateSpotLightData,
195
- } from './components/light-helpers';
196
- import {
197
- type ExtractedEnvironment,
198
- selectEnvironmentSource,
199
- } from './environment/extracted-environment';
200
- import {
201
- FogOwnerConflictError,
202
- FogParametersInvalidError,
203
182
  MaterialSkinAttrMissingError,
204
- type RenderError,
183
+ ShadowInvalidConfigError,
205
184
  SkinMaterialMismatchError,
206
185
  } from './errors/render';
207
- import {
208
- type CsmCameraData,
209
- computeDirectionalCsm,
210
- type DirectionalCsmConfig,
211
- type DirectionalCsmResult,
212
- pssmSplit,
213
- } from './extract/directional-csm';
214
- import {
215
- createMaterialInterfaceOwner,
216
- type DerivedMaterialSlotProjection,
217
- defaultMaterialTextureCoordinates,
218
- deriveMaterialSlotProjection,
219
- hasShadowCasterPass,
220
- resolveMaterialFromPort,
221
- } from './extract/material-interface';
222
- import {
223
- deriveParamSchemaForCensus,
224
- type ParamSchemaProjectionCensus,
225
- } from './param-schema-projection-census';
226
- import {
227
- buildRenderCandidateDomain,
228
- type RenderCandidateDomain,
229
- type RenderDispatchMembership,
230
- renderCandidateCsmCandidates,
231
- } from './render-dispatch-membership';
232
-
233
- export {
234
- type CascadeVolume,
235
- type CascadeVolumeSet,
236
- CascadeVolumeSetBuilder,
237
- type CsmCameraData,
238
- computeDirectionalCsm,
239
- type DirectionalCsmConfig,
240
- type DirectionalCsmResult,
241
- directionalCascadeAtlasSize,
242
- directionalCascadeRasterViewport,
243
- pssmSplit,
244
- } from './extract/directional-csm';
245
-
246
186
  import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
247
187
  import type {
248
188
  RenderFeatureHiddenEntityReport,
249
189
  RenderFeatureWorldVisibilitySnapshot,
250
190
  } from './features/types';
251
191
  import { ensureGlyphMeshMaterialSlots } from './glyph-text-layout-system';
252
- import type { RenderSceneFactInput } from './gpu-scene-schema';
253
192
  import {
254
193
  type MeshMaterialBindingDiagnostic,
255
194
  type MeshMaterialBindingSource,
256
195
  resolveMeshMaterialBindings,
257
196
  } from './mesh-material-bindings';
197
+ import { isStandardPbrMaterialShader } from './pbr-pipeline';
198
+ import { expandPointsLinesBounds } from './points-lines/bounds';
199
+ import type { PointsLinesRetainedSnapshot, PointsLinesStyle } from './points-lines/snapshot';
200
+ import { type CameraSnapshot, STANDARD_TONEMAP_FEATURE_ID } from './render-contract';
258
201
  import { getActiveCamera, selectActiveCameraIndex } from './systems/active-camera';
259
202
  import { selectPasses } from './systems/pass-selector';
260
203
  import type { SkinPaletteAllocator } from './systems/skin-palette-allocator';
261
-
262
- export interface CameraSnapshot {
263
- /** Renderer-local World attachment identity. */
264
- readonly worldId: number;
265
- /** Packed ECS entity handle; its high bits retain the entity generation. */
266
- readonly entityKey: number;
267
- /** World-space camera translation (mat4.getTranslation of Transform.world). */
268
- readonly position: Vec3;
269
- /**
270
- * Resolved world-space camera mat4 (column-major 16 floats), copied from the
271
- * entity's `Transform.world` view. The record stage derives the view matrix
272
- * as `mat4.invert(world)` (feat-20260601 D-3: read world mat4 -> invert; no
273
- * recompose from decomposed TRS).
274
- */
275
- readonly world: Float32Array;
276
- readonly fov: number;
277
- readonly aspect: number;
278
- readonly near: number;
279
- readonly far: number;
280
- /**
281
- * Camera projection variant (`'perspective'` | `'orthographic'`),
282
- * surfaced through the closed string-literal union narrowed from the
283
- * Camera schema's `projection` f32 discriminator. Drives the CSM
284
- * frustum-corner builder in render-system-extract: perspective uses
285
- * mat4.perspective(fov, aspect, ...); orthographic uses mat4.ortho
286
- * (left, right, bottom, top, ...). Without this discrimination an
287
- * orthographic camera (fov=0) would feed a degenerate matrix into
288
- * the CSM AABB-fit and the shadow atlas would never be written
289
- * (feat-20260613 M6 / w20 fix).
290
- */
291
- readonly projection: 'perspective' | 'orthographic';
292
- /** Orthographic left frustum plane (only consumed when projection === 'orthographic'). */
293
- readonly orthoLeft: number;
294
- /** Orthographic right frustum plane. */
295
- readonly orthoRight: number;
296
- /** Orthographic bottom frustum plane. */
297
- readonly orthoBottom: number;
298
- /** Orthographic top frustum plane. */
299
- readonly orthoTop: number;
300
- /**
301
- * feat-20260519-tonemap-reinhard-mvp / M3 / T-M3.1: tone-map mode
302
- * surfaced through the closed string-literal union (`tonemapFromF32`
303
- * narrows the schema's `f32` discriminator). The record stage branches
304
- * on this field to pick the geometry pipeline target format
305
- * (`bgra8unorm-srgb` for `'none'` / `'rgba16float'` for
306
- * `'reinhard-extended'`) and to decide whether to emit the post-process
307
- * tonemap fullscreen pass.
308
- */
309
- readonly tonemap: Tonemap;
310
- /** Linear pre-multiplier applied before the luminance compute (T-M3.3). */
311
- readonly exposure: number;
312
- /** Bright-end break point Lw for the extended Reinhard curve (T-M3.3). */
313
- readonly whitePoint: number;
314
- /**
315
- * feat-20260528-fxaa-post-processing / w3: anti-alias mode surfaced
316
- * through the closed string-literal union (antialiasFromF32 narrows
317
- * the schema's f32 discriminator). The record stage branches on this
318
- * field to decide whether to emit the FXAA post-process pass.
319
- */
320
- readonly antialias: Antialias;
321
- /** Explicit authoring fence for discontinuous view changes. */
322
- readonly historyVersion: number;
323
- /**
324
- * feat-20260531-bloom-first-declarative-render-graph-pass / w4: bloom
325
- * enabled discriminator surfaced through the closed string-literal union
326
- * (bloomEnabledFromF32 narrows the schema's f32 discriminator). The record
327
- * stage branches on this field to decide whether to emit the bloom
328
- * post-process pipeline.
329
- */
330
- readonly bloom: BloomEnabled;
331
- /** HDR luminance threshold for bright-pass extraction (D-3 default 1.0). */
332
- readonly bloomThreshold: number;
333
- /** Multiplier applied to the blurred bloom contribution in composite (D-3 default 1.0). */
334
- readonly bloomIntensity: number;
335
- /** Gaussian blur kernel radius, clamped [1.0, 4.0] in shader (D-3 default 4.0). */
336
- readonly bloomBlurRadius: number;
337
- /**
338
- * feat-20260709 M3 / D-3: clear-color RGBA sourced from the Camera entity's
339
- * inline `array<f32,4>` column (first-archetype-hit per OOS-2). Collapsed
340
- * from the feat-20260608 clearR/G/B/A snapshot quartet into one field aligned
341
- * with the light snapshot shape. AI users override per-camera by setting
342
- * `clearColor` when spawning the Camera entity; default is opaque black
343
- * `[0, 0, 0, 1]` (D-5). The record stage reads these values verbatim into
344
- * the LoadOp::Clear color slot.
345
- */
346
- readonly clearColor: readonly [number, number, number, number];
347
- }
348
-
349
- export type RenderVisibilityViewKind = 'main' | 'cascade';
350
-
351
- /**
352
- * One of the four CPU visibility views. Cascade membership is copied from the
353
- * canonical CascadeVolumeSet; this descriptor never derives a second volume.
354
- */
355
- export interface RenderVisibilityViewDescriptor {
356
- readonly viewId: string;
357
- readonly kind: RenderVisibilityViewKind;
358
- readonly cascadeIndex: number | undefined;
359
- readonly planes: Float32Array;
360
- /** Conservative world AABB used by the persistent spatial owner. */
361
- readonly broadphaseWorldAabb?: Float32Array | undefined;
362
- /** Canonical raster matrix for a cascade; absent for the main view. */
363
- readonly lightViewProj?: Float32Array | undefined;
364
- readonly casterCandidateIndices: ReadonlySet<number>;
365
- readonly volumeRevision: number | undefined;
366
- readonly spatialIndexRevision: number | undefined;
367
- }
368
-
369
- function cameraVisibilityPlanes(camera: CameraSnapshot): Float32Array {
370
- if (
371
- (camera.projection === 'perspective' && (camera.fov <= 0 || camera.aspect <= 0)) ||
372
- !Number.isFinite(camera.near) ||
373
- !Number.isFinite(camera.far) ||
374
- camera.near >= camera.far
375
- ) {
376
- return new Float32Array(0);
377
- }
378
- const projection = mat4.create();
379
- if (camera.projection === 'orthographic') {
380
- mat4.orthographic(
381
- projection,
382
- camera.orthoLeft,
383
- camera.orthoRight,
384
- camera.orthoBottom,
385
- camera.orthoTop,
386
- camera.near,
387
- camera.far,
388
- );
389
- } else {
390
- mat4.perspective(projection, camera.fov, camera.aspect, camera.near, camera.far);
391
- }
392
- const view = mat4.invert(mat4.create(), camera.world);
393
- return frustum.fromViewProjection(
394
- frustum.create(),
395
- mat4.multiply(mat4.create(), projection, view),
396
- );
397
- }
398
-
399
- function cameraVisibilityBroadphaseAabb(camera: CameraSnapshot): Float32Array | undefined {
400
- if (
401
- (camera.projection === 'perspective' && (camera.fov <= 0 || camera.aspect <= 0)) ||
402
- !Number.isFinite(camera.near) ||
403
- !Number.isFinite(camera.far) ||
404
- camera.near >= camera.far
405
- ) {
406
- return undefined;
407
- }
408
- const projection = mat4.create();
409
- if (camera.projection === 'orthographic') {
410
- mat4.orthographic(
411
- projection,
412
- camera.orthoLeft,
413
- camera.orthoRight,
414
- camera.orthoBottom,
415
- camera.orthoTop,
416
- camera.near,
417
- camera.far,
418
- );
419
- } else {
420
- mat4.perspective(projection, camera.fov, camera.aspect, camera.near, camera.far);
421
- }
422
- const view = mat4.invert(mat4.create(), camera.world);
423
- const inverseViewProjection = mat4.invert(
424
- mat4.create(),
425
- mat4.multiply(mat4.create(), projection, view),
426
- );
427
- const span = camera.far - camera.near;
428
- const nearNdc =
429
- camera.projection === 'orthographic'
430
- ? 0
431
- : (camera.far * (camera.near - camera.near)) / (camera.near * span);
432
- const farNdc =
433
- camera.projection === 'orthographic'
434
- ? 1
435
- : (camera.far * (camera.far - camera.near)) / (camera.far * span);
436
- let minX = Infinity;
437
- let minY = Infinity;
438
- let minZ = Infinity;
439
- let maxX = -Infinity;
440
- let maxY = -Infinity;
441
- let maxZ = -Infinity;
442
- for (const ndcZ of [nearNdc, farNdc]) {
443
- for (const ndcX of [-1, 1]) {
444
- for (const ndcY of [-1, 1]) {
445
- const point = mat4.unproject(
446
- vec3.create(),
447
- new Float32Array([ndcX, ndcY, ndcZ]),
448
- inverseViewProjection,
449
- );
450
- if (![point[0], point[1], point[2]].every(Number.isFinite)) return undefined;
451
- minX = Math.min(minX, point[0] ?? 0);
452
- minY = Math.min(minY, point[1] ?? 0);
453
- minZ = Math.min(minZ, point[2] ?? 0);
454
- maxX = Math.max(maxX, point[0] ?? 0);
455
- maxY = Math.max(maxY, point[1] ?? 0);
456
- maxZ = Math.max(maxZ, point[2] ?? 0);
457
- }
458
- }
459
- }
460
- return new Float32Array([minX, minY, minZ, maxX, maxY, maxZ]);
461
- }
462
-
463
- /** Build the main camera plus exactly three CSM view descriptors. */
464
- export function buildRenderVisibilityViewDescriptors(input: {
465
- readonly camera: CameraSnapshot | undefined;
466
- readonly directionalCsm: DirectionalCsmResult | undefined;
467
- readonly spatialIndexRevision?: number | undefined;
468
- }): readonly RenderVisibilityViewDescriptor[] {
469
- const main = {
470
- viewId: 'main',
471
- kind: 'main' as const,
472
- cascadeIndex: undefined,
473
- planes: input.camera === undefined ? new Float32Array(0) : cameraVisibilityPlanes(input.camera),
474
- broadphaseWorldAabb:
475
- input.camera === undefined ? undefined : cameraVisibilityBroadphaseAabb(input.camera),
476
- lightViewProj: undefined,
477
- casterCandidateIndices: new Set<number>(),
478
- volumeRevision: undefined,
479
- spatialIndexRevision: input.spatialIndexRevision,
480
- } satisfies RenderVisibilityViewDescriptor;
481
- const cascades = Array.from({ length: 3 }, (_, cascadeIndex) => {
482
- const volume = input.directionalCsm?.volumes[cascadeIndex];
483
- return {
484
- viewId: `cascade-${cascadeIndex}`,
485
- kind: 'cascade' as const,
486
- cascadeIndex,
487
- // CascadeVolumeSet owns the exact boundary and membership. The CPU
488
- // executor consumes the stable candidate IDs below instead of fitting a
489
- // second frustum or re-running CSM math.
490
- planes:
491
- volume === undefined
492
- ? new Float32Array(0)
493
- : frustum.fromViewProjection(frustum.create(), volume.lightViewProj),
494
- broadphaseWorldAabb:
495
- volume === undefined ? undefined : new Float32Array(volume.casterBroadphaseWorldAabb),
496
- lightViewProj: volume === undefined ? undefined : new Float32Array(volume.lightViewProj),
497
- casterCandidateIndices: new Set(volume?.casterRenderableIndices ?? []),
498
- volumeRevision: volume?.revision,
499
- spatialIndexRevision: input.spatialIndexRevision,
500
- } satisfies RenderVisibilityViewDescriptor;
501
- });
502
- return Object.freeze([main, ...cascades]);
503
- }
204
+ import type { SkinPaletteSlice } from './systems/skin-palette-types';
504
205
 
505
206
  /**
506
207
  * DirectionalLightSnapshot — sun-like infinite light variant of the
@@ -613,8 +314,6 @@ export interface PointShadowSnapshot {
613
314
  */
614
315
  export interface SpotLightSnapshot {
615
316
  readonly kind: 'spot';
616
- /** ECS entity handle in the source World; diagnostic identity only. */
617
- readonly entity: number;
618
317
  readonly position: Vec3;
619
318
  readonly direction: Vec3;
620
319
  readonly color: Vec3;
@@ -744,10 +443,6 @@ export interface ExtractedLights {
744
443
  * when castShadow=false or no directional light.
745
444
  */
746
445
  readonly directionalCsmDirection: Vec3 | undefined;
747
- /** Canonical receiver/caster plan consumed by persistent scene and record. */
748
- readonly directionalCsm?: DirectionalCsmResult;
749
- /** Internal compatibility projection; the plan above remains canonical. */
750
- readonly cascades?: DirectionalCsmResult['cascades'];
751
446
  }
752
447
 
753
448
  /**
@@ -757,7 +452,7 @@ export interface ExtractedLights {
757
452
  * `equirectHandle` carries the packed u32 handle for
758
453
  * `Handle<EquirectAsset, 'shared'>`; the record stage drives the internal
759
454
  * lazy cubemap projection from it and resolves the projected GPU cubemap via
760
- * `GpuResourceStore.getCubemapGpuView(...)` and related helpers
455
+ * `GpuResidencyCache.getCubemapGpuView(...)` and related helpers
761
456
  * (feat-20260630 M3 / w16-w18). The snapshot carries NO projection status
762
457
  * field: the status truth lives in the store's CubemapGpuEntry (D-3 SSOT);
763
458
  * record queries it once per frame.
@@ -781,128 +476,13 @@ export interface SkylightSnapshot {
781
476
  readonly entityHandle: number;
782
477
  }
783
478
 
784
- /** Retained scene-radiance Fog facts; parameters remain outside inspection. */
785
- export interface FogSnapshot {
786
- readonly color: readonly [number, number, number];
787
- readonly density: number;
788
- readonly heightFalloff: number;
789
- readonly maxOpacity: number;
790
- readonly revision: number;
791
- }
792
-
793
- /** Internal owner facts needed to publish a Fog snapshot transactionally. */
794
- export interface FogSelection {
795
- readonly snapshot: FogSnapshot | undefined;
796
- readonly resourceOwner: number;
797
- readonly entity: EntityHandle | undefined;
798
- }
799
-
800
- export type FogSelectionError = Extract<
801
- RenderError,
802
- FogOwnerConflictError | FogParametersInvalidError
803
- >;
804
-
805
- export type FogSelectionResult = Result<FogSelection, FogSelectionError>;
806
-
807
- const FOG_ENTITY_DETAIL_LIMIT = 8;
808
-
809
- function fogRevision(world: World, resourceOwner: number, entity: EntityHandle): number {
810
- let hash = 2166136261 >>> 0;
811
- const fog = world.get(entity, Fog);
812
- const source = fog.ok
813
- ? `${world.identity}:${resourceOwner}:${entity}:${Array.from(fog.value.color).join(',')}:${fog.value.density}:${fog.value.heightFalloff}:${fog.value.maxOpacity}`
814
- : `${world.identity}:${resourceOwner}:${entity}`;
815
- for (let index = 0; index < source.length; index += 1) {
816
- hash ^= source.charCodeAt(index);
817
- hash = Math.imul(hash, 16777619) >>> 0;
818
- }
819
- return hash === 0 ? 1 : hash;
820
- }
821
-
822
- function fogInvalid(
823
- entity: EntityHandle,
824
- field: string,
825
- expected: string,
826
- actual: unknown,
827
- ): FogSelectionResult {
828
- return err(new FogParametersInvalidError(entity as number, field, expected, actual));
829
- }
830
-
831
- /** Select and validate the single Fog authoring entity from resourceOwner. */
832
- export function selectFog(world: World | undefined, resourceOwner: number): FogSelectionResult {
833
- if (world === undefined) return ok({ snapshot: undefined, resourceOwner, entity: undefined });
834
- const entities: EntityHandle[] = [];
835
- const query = world.query({ read: [Fog] }).unwrap();
836
- for (const row of query) entities.push(row.entity);
837
- if (entities.length === 0) {
838
- return ok({ snapshot: undefined, resourceOwner, entity: undefined });
839
- }
840
- if (entities.length > 1) {
841
- const bounded = entities.slice(0, FOG_ENTITY_DETAIL_LIMIT).map((entity) => entity as number);
842
- return err(
843
- new FogOwnerConflictError(
844
- resourceOwner,
845
- bounded,
846
- Math.max(0, entities.length - bounded.length),
847
- ),
848
- );
849
- }
850
-
851
- const entity = entities[0];
852
- if (entity === undefined) return ok({ snapshot: undefined, resourceOwner, entity: undefined });
853
- const read = world.get(entity, Fog);
854
- if (!read.ok) return ok({ snapshot: undefined, resourceOwner, entity: undefined });
855
- const color = read.value.color;
856
- if (
857
- color.length !== 3 ||
858
- color.some((value) => typeof value !== 'number' || !Number.isFinite(value) || value < 0)
859
- ) {
860
- return fogInvalid(
861
- entity,
862
- 'color',
863
- 'a three-component finite non-negative array',
864
- Array.from(color),
865
- );
866
- }
867
- const density = read.value.density;
868
- if (!Number.isFinite(density) || density < 0) {
869
- return fogInvalid(entity, 'density', 'a finite non-negative number', density);
870
- }
871
- const heightFalloff = read.value.heightFalloff;
872
- if (!Number.isFinite(heightFalloff) || heightFalloff < 0) {
873
- return fogInvalid(entity, 'heightFalloff', 'a finite non-negative number', heightFalloff);
874
- }
875
- const maxOpacity = read.value.maxOpacity;
876
- if (!Number.isFinite(maxOpacity) || maxOpacity < 0 || maxOpacity > 1) {
877
- return fogInvalid(entity, 'maxOpacity', 'a finite number in [0, 1]', maxOpacity);
878
- }
879
- const normalizedColor: [number, number, number] = [color[0] ?? 0, color[1] ?? 0, color[2] ?? 0];
880
- const snapshot: FogSnapshot = Object.freeze({
881
- color: normalizedColor,
882
- density,
883
- heightFalloff,
884
- maxOpacity,
885
- revision: fogRevision(world, resourceOwner, entity),
886
- });
887
- return ok({ snapshot, resourceOwner, entity });
888
- }
889
-
890
- function routeFogSelectionError(world: World | undefined, error: FogSelectionError): void {
891
- if (world === undefined) return;
892
- try {
893
- routeWorldError(world, error, { systemName: 'RenderSystem.extractFog' });
894
- } catch {
895
- // World error policy owns whether a routed error throws.
896
- }
897
- }
898
-
899
479
  /**
900
480
  * SkyboxSnapshot -- extract-stage view of one SkyboxBackground entity
901
481
  * (feat-20260531-skybox-env-background M2 / w5).
902
482
  *
903
483
  * `equirectHandle` carries the packed u32 handle for
904
484
  * `Handle<EquirectAsset, 'shared'>`; the record stage resolves the projected
905
- * GPU cubemap via `GpuResourceStore.getCubemapGpuView(...)`.
485
+ * GPU cubemap via `GpuResidencyCache.getCubemapGpuView(...)`.
906
486
  *
907
487
  * `mode` carries the raw `f32` column value (`SKYBOX_MODE_CUBEMAP = 0`).
908
488
  * First hit wins per plan-strategy D-6; multi-entity once-warn in record stage.
@@ -921,27 +501,9 @@ export interface SkyboxSnapshot {
921
501
  export interface RenderableSnapshot {
922
502
  readonly assetHandle: number;
923
503
  readonly transform: TransformSnapshot;
924
- /**
925
- * Bounds-only transform derived from authored local TRS when the resolved
926
- * world column is still its identity default. This preserves the producer
927
- * bounds source for direct extract-before-propagate paths without changing
928
- * the render SSOT, which remains `transform.world`.
929
- */
930
- readonly boundsWorld?: Float32Array;
931
- /** Candidate-domain copy of all instance transforms, retained before cull. */
932
- readonly candidateInstanceTransforms?: Float32Array;
933
504
  /** Finite producer-owned local bounds retained for persistent CPU view work. */
934
505
  readonly localAabb?: Float32Array;
935
- /**
936
- * Optional authoring/pack-owned scene facts. The projection supplies
937
- * explicit `unknown` defaults when this field is absent; it never infers
938
- * staticness or shadow eligibility from frame-local behavior.
939
- */
940
- readonly sceneFact?: RenderSceneFactInput;
941
- /**
942
- * Forward raster item facts consumed by the first GPU-driven rigid lane.
943
- * Each entry carries its source submesh, pass identity, and resolved queue.
944
- */
506
+ /** Indexed single-submesh facts consumed by the first GPU-driven rigid lane. */
945
507
  readonly gpuDrivenDraws?: readonly GpuDrivenDrawSnapshot[];
946
508
  /**
947
509
  * feat-20260608 M5 amend / w11-a: the entity's representative (first)
@@ -997,8 +559,6 @@ export interface RenderableSnapshot {
997
559
  * not the bare entityKey alone (D-1).
998
560
  */
999
561
  readonly entityKey: number;
1000
- /** Stable pre-main-cull candidate index used by the directional CSM plan. */
1001
- readonly candidateIndex?: number;
1002
562
  /**
1003
563
  * feat-20260514 M3 (w15): when the entity carries an `Instances` component
1004
564
  * the extract stage materialises a fresh `Float32Array` snapshot of the
@@ -1038,41 +598,22 @@ export interface RenderableSnapshot {
1038
598
  * Absent (`undefined`) means the entity is not skinned.
1039
599
  */
1040
600
  readonly skin?: SkinPaletteSlice;
1041
- /** Current morph producer weights; previous values come from the submitted projection epoch. */
1042
- readonly morphWeights?: Float32Array;
1043
- /** Previous facts from the renderer's last successful submission epoch. */
1044
- readonly temporal?: RenderableTemporalSnapshot;
601
+ /**
602
+ * ECS-owned morph weights. Presence selects the Standard Pipeline CPU
603
+ * specialized deformation lane; the asset remains the source of target
604
+ * deltas and the record stage only receives this frozen POD snapshot.
605
+ */
606
+ readonly morph?: MorphSnapshot;
607
+ /** Detached M2 authoring facts for the optional Points/Lines projection. */
608
+ readonly pointsLines?: PointsLinesRetainedSnapshot;
1045
609
  }
1046
610
 
1047
- export type RenderablePreviousSource = 'last-submitted' | 'current-seed';
1048
-
1049
- export type RenderableReactiveReason =
1050
- | 'new-slot'
1051
- | 'generation-reuse'
1052
- | 'reentered'
1053
- | 'geometry-revision'
1054
- | 'material-revision'
1055
- | 'skinning-deformation'
1056
- | 'morph-deformation';
1057
-
1058
- export interface RenderableTemporalSnapshot {
1059
- readonly previousEpoch: number | undefined;
1060
- readonly previousSource: RenderablePreviousSource;
1061
- readonly reactive: boolean;
1062
- readonly reactiveReasons: readonly RenderableReactiveReason[];
1063
- readonly previousTransform: TransformSnapshot;
1064
- readonly previousInstances: InstancesSnapshot | undefined;
1065
- readonly previousSkin: SkinPaletteSlice | undefined;
1066
- readonly previousMorphWeights: Float32Array | undefined;
611
+ export interface MorphSnapshot {
612
+ readonly weights: Float32Array;
613
+ readonly targetCount: number;
1067
614
  }
1068
615
 
1069
616
  export interface GpuDrivenDrawSnapshot {
1070
- /** Index into the resolved mesh submesh array. */
1071
- readonly submeshIndex: number;
1072
- /** Index of the corresponding material pass in the resolved pass list. */
1073
- readonly passIndex: number;
1074
- /** Effective queue copied from that pass; always resolved at extract time. */
1075
- readonly queue: number;
1076
617
  readonly kind: 'indexed' | 'non-indexed';
1077
618
  readonly first: number;
1078
619
  readonly count: number;
@@ -1083,41 +624,6 @@ export interface GpuDrivenDrawSnapshot {
1083
624
  readonly materialResourceClass: string;
1084
625
  }
1085
626
 
1086
- /**
1087
- * feat-20260523-skin-skeleton-animation M2 / T-21 + T-24:
1088
- * per-draw palette slice metadata. Joint count + byte offset into the
1089
- * shared skin palette storage buffer.
1090
- *
1091
- * plan-strategy D-10: SkinPaletteSlice naming mirroring Bevy SkinByteOffset
1092
- * but with forgeax vocabulary (byteOffset not offset, jointCount not joint_count).
1093
- */
1094
- export interface SkinPaletteSlice {
1095
- readonly jointCount: number;
1096
- /** CPU snapshot of the exact palette written for this extract epoch. */
1097
- readonly jointMatrices: Float32Array;
1098
- /**
1099
- * Byte offset INTO `buffer` for this slice. On the storage path the
1100
- * allocator returns one shared buffer + a per-entity cursor offset; on
1101
- * the uniform fallback path each slice owns its own GPU buffer (cap
1102
- * `maxUniformBufferBindingSize` 16 KiB cannot host shared dynOffset
1103
- * windows for >1 entity), so `byteOffset === 0` for every slice and
1104
- * `buffer` is the per-slice handle. Record stage reads both fields and
1105
- * does not branch on the path -- BG cache key includes `buffer`, so
1106
- * two paths converge at the same code.
1107
- */
1108
- readonly byteOffset: number;
1109
- /**
1110
- * Per-slice GPU buffer handle. M6: feat-20260612 collapsed the prior
1111
- * shared-buffer assumption (record-stage read `pipelineState
1112
- * .skinPaletteAllocator.buffer`) into a per-slice carrier so the
1113
- * uniform fallback path can return a per-entity buffer without
1114
- * branching the record stage.
1115
- */
1116
- readonly buffer: import('@forgeax/engine-rhi').Buffer;
1117
- /** Previous successful-submit palette, paired with {@link buffer}. */
1118
- readonly previousBuffer?: import('@forgeax/engine-rhi').Buffer | undefined;
1119
- }
1120
-
1121
627
  export interface InstancesSnapshot {
1122
628
  /** Packed column-major mat4 transforms (16 f32 per instance). */
1123
629
  readonly transforms: Float32Array;
@@ -1177,56 +683,6 @@ export interface TransformSnapshot {
1177
683
  readonly world: Float32Array;
1178
684
  }
1179
685
 
1180
- function authoredBoundsWorld(
1181
- world: ArrayLike<number>,
1182
- local:
1183
- | {
1184
- readonly pos: ArrayLike<number>;
1185
- readonly quat: ArrayLike<number>;
1186
- readonly scale: ArrayLike<number>;
1187
- }
1188
- | undefined,
1189
- ): Float32Array | undefined {
1190
- if (local === undefined || world.length < 16) return undefined;
1191
- let worldIsIdentity = true;
1192
- const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
1193
- for (let index = 0; index < 16; index += 1) {
1194
- const value = world[index];
1195
- if (
1196
- value === undefined ||
1197
- !Number.isFinite(value) ||
1198
- Math.abs(value - (identity[index] ?? 0)) > 1e-6
1199
- ) {
1200
- worldIsIdentity = false;
1201
- break;
1202
- }
1203
- }
1204
- if (!worldIsIdentity || local.pos.length < 3 || local.quat.length < 4 || local.scale.length < 3) {
1205
- return undefined;
1206
- }
1207
- const hasAuthoredTransform =
1208
- (local.pos[0] ?? 0) !== 0 ||
1209
- (local.pos[1] ?? 0) !== 0 ||
1210
- (local.pos[2] ?? 0) !== 0 ||
1211
- (local.quat[0] ?? 0) !== 0 ||
1212
- (local.quat[1] ?? 0) !== 0 ||
1213
- (local.quat[2] ?? 0) !== 0 ||
1214
- (local.quat[3] ?? 1) !== 1 ||
1215
- (local.scale[0] ?? 1) !== 1 ||
1216
- (local.scale[1] ?? 1) !== 1 ||
1217
- (local.scale[2] ?? 1) !== 1;
1218
- if (!hasAuthoredTransform) return undefined;
1219
- for (const values of [local.pos, local.quat, local.scale]) {
1220
- for (let index = 0; index < values.length; index += 1) {
1221
- const value = values[index];
1222
- if (value === undefined || !Number.isFinite(value)) return undefined;
1223
- }
1224
- }
1225
- const composed = mat4.create();
1226
- mat4.compose(composed, local.pos, local.quat, local.scale);
1227
- return composed;
1228
- }
1229
-
1230
686
  /**
1231
687
  * MaterialSnapshot: extract-stage view of one entity's material asset (M2 / w6
1232
688
  * of feat-20260517-merge-mesh-renderer-material-renderer; plan-strategy section 2.3).
@@ -1267,16 +723,12 @@ export interface MaterialSnapshot {
1267
723
  * discriminator (M4-T06).
1268
724
  */
1269
725
  readonly materialShaderId?: string | undefined;
1270
- /** Canonical RenderAssetPort GUID used to join the production readiness tuple. */
1271
- readonly materialGuid?: string | undefined;
1272
726
  /**
1273
727
  * Stable source handle for the material asset. Record uses this only for
1274
728
  * same-frame assembly reuse; GPU bind-group validity still comes from the
1275
729
  * resource-identity cache.
1276
730
  */
1277
731
  readonly materialHandle?: number | undefined;
1278
- /** Whether the resolved material owns a ShadowCaster pass. */
1279
- readonly shadowCaster?: boolean | undefined;
1280
732
  /**
1281
733
  * The selected forward-pass state for this exact material. A mesh can bind
1282
734
  * different materials to its submeshes, so pipeline selection must read the
@@ -1292,10 +744,6 @@ export interface MaterialSnapshot {
1292
744
  readonly paramSnapshot?: Readonly<Record<string, number | number[] | string>> | undefined;
1293
745
  /** Effective MaterialAsset parameter contract used to lay out this snapshot. */
1294
746
  readonly materialParamSchema?: readonly ParamSchemaEntry[] | undefined;
1295
- /** Typed per-slot projection produced from the effective root schema. */
1296
- readonly derivedMaterialSlot?: DerivedMaterialSlotProjection | undefined;
1297
- /** Structured extract failure that the record stage must propagate and stop. */
1298
- readonly derivedMaterialError?: MaterialError | undefined;
1299
747
  /** Authored per-slot UV set and KHR texture transform metadata. */
1300
748
  readonly textureCoordinates?: ReadonlyMap<string, MaterialTextureCoordinates> | undefined;
1301
749
  /**
@@ -1303,7 +751,7 @@ export interface MaterialSnapshot {
1303
751
  * (feat-20260621-learn-render-5-5-parallax M2 / w7). The SSOT carrier for
1304
752
  * EVERY texture the shader's `derive(paramSchema).textureFieldNames`
1305
753
  * declares — `baseColorTexture` / `metallicRoughnessTexture` /
1306
- * `normalTexture` for built-in standard-PBR, plus any custom field such as
754
+ * `normalTexture` for built-in standard-PBR, plus each custom field such as
1307
755
  * `heightTexture` (LO 5.5 parallax). The record stage iterates this map to
1308
756
  * assemble the user-region bind group per the per-shader BGL (w8), so a 4th
1309
757
  * (or Nth) texture flows end-to-end without a hardcoded field list.
@@ -1322,7 +770,7 @@ export interface MaterialSnapshot {
1322
770
  * occupies the same texture2d values slot a static texture would (P4:
1323
771
  * one binding shape), but extract routes it here instead of `textureHandles`
1324
772
  * so the record stage pulls the current-frame view from the transient
1325
- * DynamicTextureStore (D-3) instead of `GpuResourceStore.ensureResident`
773
+ * DynamicTextureStore (D-3) instead of `GpuResidencyCache.ensureResident`
1326
774
  * (which has no `video` arm; AC-08). Each entry also carries the resolved
1327
775
  * clip handle so the record stage can key the per-frame upload.
1328
776
  *
@@ -1403,7 +851,7 @@ export interface MaterialSnapshotCacheEntry {
1403
851
  readonly passes: readonly MaterialPass[];
1404
852
  /** Root/chain identities are present only on cross-frame-safe entries. */
1405
853
  readonly chain?: readonly MaterialCacheChainIdentity[];
1406
- /** RenderAssetPort epoch at which the root/parent chain was last validated. */
854
+ /** AssetRegistry epoch at which the root/parent chain was last validated. */
1407
855
  readonly catalogEpoch?: number;
1408
856
  /** True only when every object reachable from the authored chain is frozen. */
1409
857
  readonly crossFrameSafe: boolean;
@@ -1426,44 +874,11 @@ function gpuDrivenMaterialResourceClass(material: MaterialSnapshot): string {
1426
874
  });
1427
875
  }
1428
876
 
1429
- function gpuDrivenForwardPass(
1430
- material: MaterialSnapshot,
1431
- cache: MaterialSnapshotCache,
1432
- ): { readonly passIndex: number; readonly queue: number } | undefined {
1433
- // Test-only snapshots and the engine default material do not have a cached
1434
- // authored pass list. Their forward pass is the canonical Geometry pass.
1435
- if (material.materialHandle === undefined) {
1436
- return { passIndex: 0, queue: RenderQueue.Geometry };
1437
- }
1438
- if (material.materialHandle === 0) {
1439
- // The synthesized default dispatch keeps ShadowCaster at index 0 and
1440
- // Forward at index 1; use the same identity for GPU/CPU ownership.
1441
- return { passIndex: 1, queue: RenderQueue.Geometry };
1442
- }
1443
- const passes = cache.get(material.materialHandle)?.passes;
1444
- if (passes === undefined) return undefined;
1445
- for (let passIndex = 0; passIndex < passes.length; passIndex += 1) {
1446
- const pass = passes[passIndex];
1447
- if (pass === undefined) continue;
1448
- const state = pass.renderState as
1449
- | (Readonly<Record<string, unknown>> & {
1450
- readonly tags?: Readonly<Record<string, string>>;
1451
- readonly queue?: unknown;
1452
- })
1453
- | undefined;
1454
- const lightMode = state?.tags?.LightMode;
1455
- if (lightMode !== 'Forward' && pass.name !== 'forward') continue;
1456
- const queue = typeof state?.queue === 'number' ? state.queue : RenderQueue.Geometry;
1457
- return { passIndex, queue };
1458
- }
1459
- return undefined;
1460
- }
1461
-
1462
877
  // === DispatchEntry — M3 / w26 single dispatch list (feat-20260526-material-asset-multipass-renderstate) ===
1463
878
  //
1464
879
  // Plan-strategy D-3: single dispatch list sorted by queue value,
1465
880
  // replacing the old three-bucket opaque/transparent/overlay dispatch.
1466
- // Each entry carries the per-pass render-state, defines, and entry-point
881
+ // Each entry carries the per-pass render-state and entry-point
1467
882
  // data from the resolved MaterialPass so the record stage
1468
883
  // reads them without re-resolving the material.
1469
884
 
@@ -1507,6 +922,12 @@ export function sortDispatchByQueue<E extends { readonly queue: number }>(
1507
922
  return entries.slice().sort((a, b) => a.queue - b.queue);
1508
923
  }
1509
924
 
925
+ const DEFAULT_FORWARD_PASS: MaterialPass = {
926
+ name: 'forward',
927
+ program: { module: 'forgeax::default-unlit', vertexEntry: 'vs_main', fragmentEntry: 'fs_main' },
928
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
929
+ };
930
+
1510
931
  function appendMaterialDispatchEntries(
1511
932
  pendingDispatch: DispatchEntry[],
1512
933
  passes: readonly MaterialPass[],
@@ -1515,6 +936,7 @@ function appendMaterialDispatchEntries(
1515
936
  renderableIndex: number,
1516
937
  layer: number,
1517
938
  paramSnapshot: Readonly<Record<string, number | number[] | string>> | undefined,
939
+ passIndexOffset = 0,
1518
940
  ): void {
1519
941
  const matchedPasses = selectPasses(passes, {});
1520
942
  for (let pIdx = 0; pIdx < matchedPasses.length; pIdx++) {
@@ -1529,12 +951,14 @@ function appendMaterialDispatchEntries(
1529
951
  entityIndex: entity,
1530
952
  materialHandle,
1531
953
  renderableIndex,
1532
- passIndex: pIdx,
1533
- queue: passState.queue ?? RenderQueue.Geometry,
954
+ passIndex: passIndexOffset + pIdx,
955
+ queue: passState.queue ?? 2000,
1534
956
  layer,
1535
957
  tags: passState.tags ?? {},
1536
958
  renderState: pipelineRenderState(passState),
1537
- defines: pass.program.moduleSlots,
959
+ // Material module identity is already closed in the cooked program.
960
+ // It must never become a draw-time define map.
961
+ defines: undefined,
1538
962
  vertexEntry: pass.program.vertexEntry,
1539
963
  fragmentEntry: pass.program.fragmentEntry,
1540
964
  materialShaderId: runtimeMaterialShaderId(pass.program.module, pass.name),
@@ -1550,23 +974,16 @@ export interface ExtractedFrame {
1550
974
  readonly cameras: CameraSnapshot[];
1551
975
  readonly lights: ExtractedLights;
1552
976
  readonly renderables: RenderableSnapshot[];
1553
- /** Author-visible snapshots retained before the main-view frustum cull. */
1554
- readonly candidateRenderables?: readonly RenderableSnapshot[];
1555
- /** Pass dispatch for the same pre-main-cull candidate domain. */
1556
- readonly candidateDispatch?: readonly DispatchEntry[];
1557
- /** Shared membership owner for the candidate dispatch domain. */
1558
- readonly candidateDispatchMembership?: RenderDispatchMembership;
1559
- /** Shared immutable candidate array/dispatch/index owner. */
1560
- readonly candidateDomain?: RenderCandidateDomain;
1561
- /** Resource-owner Fog snapshot retained for direct extract consumers. */
1562
- readonly fog?: FogSnapshot | undefined;
1563
977
  /**
1564
978
  * Single dispatch list sorted by queue value (ascending, stable sort).
1565
979
  * Replaces the old opaqueDispatch / transparentDispatch / overlayDispatch
1566
980
  * three-bucket model per plan-strategy D-3.
1567
981
  */
1568
982
  readonly dispatch: DispatchEntry[];
1569
- readonly environment: ExtractedEnvironment;
983
+ readonly skylight: SkylightSnapshot | undefined;
984
+ readonly skylightCount: number;
985
+ readonly skybox: SkyboxSnapshot | undefined;
986
+ readonly skyboxCount: number;
1570
987
  /**
1571
988
  * feat-20260528-frustum-culling M3 / w11: frustum culling statistics
1572
989
  * collected during the extract phase. `total` is the count of entities
@@ -1593,53 +1010,82 @@ export interface ExtractedFrame {
1593
1010
  }
1594
1011
 
1595
1012
  /**
1596
- * Transparent-bucket dispatch entry extends the dispatch snapshot with
1597
- * per-entity sort inputs precomputed at extract time.
1598
- *
1599
- * Field semantics:
1600
- * - `layer` — primary sort key (signed i32 from `Layer.value`; default 0
1601
- * for entities without a Layer component).
1602
- * - `posX` / `posY` / `posZ` — world position from the entity's Transform.
1603
- * - `pivotY` / `sizeY` — sprite's Y-axis foot offset inputs. `pivotY =
1604
- * SpriteMaterialAsset.pivot[1]` (default 0.5); `sizeY` = Transform's
1605
- * world scale Y (sprite quad's world-space height in unit-quad world
1606
- * space — the unit quad is 1x1 so `Transform.scaleY` directly maps to
1607
- * world height). The foot-Y formula `posY - pivotY * sizeY` is the
1608
- * SSOT for the JRPG Y-sort path (mode=1, requirements §AC-10).
1609
- * - `sortKey` — optional per-entity override from `SortKey.value`. When
1610
- * present, `transparentSortEntries` uses it INSTEAD of the mode formula
1611
- * for this entry (still gated by `layer` as the primary key).
1612
- *
1613
- * Plan-strategy §3.3 interface example + §AC-10 sort path + §AC-19
1614
- * derivation row 13 (this is the true new POD shape `@new-surface`; not a
1615
- * mere rename of an existing snapshot).
1013
+ * Internal world surface used by extract for archetype-graph traversal and
1014
+ * error routing. The packed `Entity` handle for a row is read directly from
1015
+ * the essential id=0 `Entity` column (`arch.columns.get(Entity.id).get('self')`)
1016
+ * -- no generation lookup / encodeEntity rebuild (feat-20260602 M2).
1616
1017
  */
1617
- export interface TransparentEntry {
1618
- readonly entityIndex: number;
1619
- readonly materialHandle: number;
1620
- readonly layer: number;
1621
- readonly posX: number;
1622
- readonly posY: number;
1623
- readonly posZ: number;
1624
- readonly pivotY: number;
1625
- readonly sizeY: number;
1626
- // `number | undefined` rather than the strict `?: number` form so AI
1627
- // users that construct TransparentEntry literals with
1628
- // `sortKey: maybeUndefined` (the typical SoA projection pattern) do
1629
- // not trip `exactOptionalPropertyTypes: true` (the engine's tsconfig
1630
- // base). Absent === undefined consumer semantics are identical; the
1631
- // sort helper checks `e.sortKey !== undefined` either way.
1632
- readonly sortKey?: number | undefined;
1633
- /**
1634
- * Index into the parallel `ExtractedFrame.renderables[]` array. M-3 /
1635
- * w25 record stage uses this to look up the `RenderableSnapshot`
1636
- * (Transform + mesh + material snapshot) belonging to this transparent
1637
- * entry after `transparentSortEntries` reorders the bucket. Optional
1638
- * for test fixtures that construct TransparentEntry literals by hand
1639
- * (w16 sort tests bypass renderables[] correlation); the real extract
1640
- * path always sets a non-negative integer index here.
1641
- */
1642
- readonly renderableIndex?: number | undefined;
1018
+ type RenderErrorContext = {
1019
+ readonly systemName: string;
1020
+ /** Retained as diagnostic metadata for legacy render call sites. */
1021
+ readonly severity?: 'error' | 'warning';
1022
+ };
1023
+
1024
+ type ArrayFieldView = ArrayLike<number>;
1025
+
1026
+ /**
1027
+ * Render's old zero-copy ECS calls now use the explicit public projection
1028
+ * boundary. The optimized ECS owner keeps storage behind `World`; render
1029
+ * receives a detached array snapshot and routes failures through the same
1030
+ * projection owner.
1031
+ */
1032
+ type WorldInternalView = {
1033
+ _routeError(error: unknown, ctx: RenderErrorContext): void;
1034
+ _getArrayView<N extends string, S extends ComponentSchema>(
1035
+ entity: EntityHandle,
1036
+ component: Component<N, S>,
1037
+ fieldName: string,
1038
+ ): ArrayFieldView | undefined;
1039
+ };
1040
+
1041
+ const Severity = Object.freeze({ Error: 'error', Warning: 'warning' } as const);
1042
+
1043
+ function createWorldInternalView(world: World): WorldInternalView {
1044
+ return {
1045
+ _routeError(error, ctx) {
1046
+ routeWorldError(world, error, { systemName: ctx.systemName });
1047
+ },
1048
+ _getArrayView(entity, component, fieldName) {
1049
+ return readRenderArrayView(world, entity, component, fieldName) as ArrayFieldView | undefined;
1050
+ },
1051
+ };
1052
+ }
1053
+
1054
+ /**
1055
+ * The user-region texture fields the built-in standard-PBR material declares.
1056
+ * Used as the fallback texture-field set when the shader id is not registered
1057
+ * (cross-worktree shader-late-register, plan R-4) so a built-in material still
1058
+ * resolves its user-region textures. Mirrors `derive(default-standard-pbr).textureFieldNames`.
1059
+ */
1060
+ const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
1061
+ 'baseColorTexture',
1062
+ 'metallicRoughnessTexture',
1063
+ 'normalTexture',
1064
+ 'specularTintTexture',
1065
+ ];
1066
+ const BUILTIN_USER_REGION_TEXTURE_FIELD_SET = new Set(BUILTIN_USER_REGION_TEXTURE_FIELDS);
1067
+ const BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET = new Set(['baseColorTexture']);
1068
+
1069
+ function materialTextureFields(
1070
+ shaderId: string | undefined,
1071
+ fields: ReadonlySet<string> | undefined,
1072
+ ): ReadonlySet<string> | undefined {
1073
+ if (fields !== undefined && fields.size > 0) return fields;
1074
+ if (
1075
+ shaderId === 'forgeax::default-standard-pbr' ||
1076
+ shaderId === 'forgeax::default-standard-pbr-skin' ||
1077
+ shaderId === 'forgeax::pbr-skin'
1078
+ ) {
1079
+ return BUILTIN_USER_REGION_TEXTURE_FIELD_SET;
1080
+ }
1081
+ if (
1082
+ shaderId === 'forgeax::default-unlit' ||
1083
+ shaderId === 'forgeax::sprite' ||
1084
+ shaderId === 'forgeax::sprite-lit'
1085
+ ) {
1086
+ return BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET;
1087
+ }
1088
+ return fields;
1643
1089
  }
1644
1090
 
1645
1091
  /**
@@ -1724,12 +1170,12 @@ function pipelineRenderState(
1724
1170
 
1725
1171
  /**
1726
1172
  * tweak-20260627-model-loading-smoke-build-perf M4: per-World intern cache for
1727
- * the typed AssetRegistry.load texture/sampler resolution path. A MaterialAsset's
1173
+ * the loadByGuid texture/sampler resolution path. A MaterialAsset's
1728
1174
  * texture/sampler values remain embedded GUID strings (dash-form) after
1729
- * AssetRegistry.load; the extract stage re-resolves each GUID to a column handle every
1175
+ * loadByGuid; the extract stage re-resolves each GUID to a column handle every
1730
1176
  * frame. Before this cache each resolution called `world.allocSharedRef`, which
1731
1177
  * mints a NEW monotonically-increasing slot id per call. Because the GPU
1732
- * residency cache (`GpuResourceStore.textureGpuHandles`) is keyed on
1178
+ * residency cache (`GpuResidencyCache.textureGpuHandles`) is keyed on
1733
1179
  * `handleSlot(handle)`, a fresh slot every frame meant the residency check
1734
1180
  * ALWAYS missed -> all textures re-uploaded to the GPU every frame, old GPU
1735
1181
  * textures never freed (refcount never hits 0). Unbounded GPU memory +
@@ -1740,11 +1186,10 @@ function pipelineRenderState(
1740
1186
  * across frames (architecture-principle §6 idempotency: same GUID resolved N
1741
1187
  * times yields the same handle). The handle is intentionally long-lived -- it
1742
1188
  * lives as long as the World references the material, which is exactly the
1743
- * desired lifetime. Final release is published as structured evidence by the
1744
- * World-owned SharedRefStore; the renderer owner consumes that evidence and
1745
- * owns GPU disposal.
1189
+ * desired lifetime; the `onLastRelease` -> `gpuStore.evictTexture` wiring stays
1190
+ * coherent because the handle is no longer churned per frame.
1746
1191
  *
1747
- * Invariant boundary (asset-registry.ts:1958-1962): the RenderAssetPort is a
1192
+ * Invariant boundary (asset-registry.ts:1958-1962): the AssetRegistry is a
1748
1193
  * GUID -> payload catalogue with NO handle/World concept -- it cannot mint a
1749
1194
  * column handle. So this intern cache lives in the extract/render layer, keyed
1750
1195
  * per-World via a WeakMap (the World owns the SharedRefStore that mints slots).
@@ -1767,7 +1212,7 @@ const guidHandleInternByWorld = new WeakMap<World, Map<string, GuidHandleInternE
1767
1212
 
1768
1213
  function internSharedRefFromGuid<B extends string>(
1769
1214
  world: World,
1770
- assetsRef: RenderAssetPort,
1215
+ assetsRef: AssetRegistry,
1771
1216
  guid: string,
1772
1217
  brand: B,
1773
1218
  ): Handle<B, 'shared'> | undefined {
@@ -1780,19 +1225,19 @@ function internSharedRefFromGuid<B extends string>(
1780
1225
  const cached = perWorld.get(key);
1781
1226
  const payload = assetsRef.lookup(guid);
1782
1227
  if (cached !== undefined && cached.payload === payload) {
1783
- const cachedHandle = cached.handle as unknown as Handle<B, 'shared'>;
1228
+ const cachedHandle = toShared<B>(cached.handle);
1784
1229
  if (world.sharedRefs.resolve(cachedHandle).ok) return cachedHandle;
1785
1230
  }
1786
1231
  if (cached !== undefined) {
1787
- world.sharedRefs.release(cached.handle as unknown as Handle<B, 'shared'>);
1232
+ world.sharedRefs.release(toShared<B>(cached.handle));
1788
1233
  perWorld.delete(key);
1789
1234
  }
1790
1235
  if (payload === undefined) return undefined;
1791
- // Mint exactly once per (world, guid, brand). The World-owned store
1792
- // publishes structured final-release evidence; disposal remains with the
1793
- // renderer owner effect rather than a per-handle callback.
1794
- const handle = world.allocSharedRef(brand, payload);
1795
- perWorld.set(key, { handle: handle as unknown as number, payload });
1236
+ // Mint exactly once per (world, guid, brand). ECS owns the allocation grant
1237
+ // and publishes release evidence through SharedRefStore; render does not
1238
+ // attach a second deleter lifecycle to the World.
1239
+ const handle = world.internSharedRef(brand, payload);
1240
+ perWorld.set(key, { handle, payload });
1796
1241
  return handle;
1797
1242
  }
1798
1243
 
@@ -1817,7 +1262,7 @@ function internSharedRefFromGuid<B extends string>(
1817
1262
  function resolveVideoFieldHandle(
1818
1263
  value: unknown,
1819
1264
  world: World,
1820
- assetsRef: RenderAssetPort,
1265
+ assetsRef: AssetRegistry,
1821
1266
  ): Handle<'VideoAsset', 'shared'> | undefined {
1822
1267
  const texture = materialTextureValue(value);
1823
1268
  const textureGuid = assetReferenceText(texture?.texture ?? value);
@@ -1843,11 +1288,14 @@ function materialTextureValue(value: unknown): MaterialTextureValue | undefined
1843
1288
 
1844
1289
  function assetReferenceText(value: unknown): string | undefined {
1845
1290
  if (typeof value === 'string') return value;
1846
- if (value instanceof Uint8Array && value.byteLength === 16)
1847
- return AssetGuid.format(value as never);
1291
+ if (value instanceof Uint8Array && isAssetGuidBytes(value)) return AssetGuid.format(value);
1848
1292
  return undefined;
1849
1293
  }
1850
1294
 
1295
+ function isAssetGuidBytes(value: Uint8Array): value is AssetGuidBytes {
1296
+ return value.byteLength === 16;
1297
+ }
1298
+
1851
1299
  function materialNormalScale(values: Readonly<Record<string, unknown>>): number {
1852
1300
  for (const [field, value] of Object.entries(values)) {
1853
1301
  if (field !== 'normalTexture') continue;
@@ -1863,14 +1311,16 @@ function materialTextureRef(value: unknown): unknown {
1863
1311
 
1864
1312
  function collectMaterialTextureCoordinates(
1865
1313
  values: Readonly<Record<string, unknown>>,
1866
- schema: readonly ParamSchemaEntry[],
1867
- derivedInterface?: DerivedMaterialInterface,
1868
- paramSchemaProjectionCensus?: ParamSchemaProjectionCensus,
1869
1314
  ): Map<string, MaterialTextureCoordinates> {
1870
1315
  const out = new Map<string, MaterialTextureCoordinates>();
1871
- for (const field of (
1872
- derivedInterface ?? deriveParamSchemaForCensus(schema, paramSchemaProjectionCensus, 'submesh')
1873
- ).textureFieldNames) {
1316
+ for (const field of [
1317
+ 'baseColorTexture',
1318
+ 'metallicRoughnessTexture',
1319
+ 'normalTexture',
1320
+ 'specularTintTexture',
1321
+ 'emissiveTexture',
1322
+ 'occlusionTexture',
1323
+ ]) {
1874
1324
  const coordinates = materialTextureValue(values[field])?.coordinates;
1875
1325
  if (coordinates !== undefined) out.set(field, coordinates);
1876
1326
  }
@@ -1897,14 +1347,14 @@ function collectMaterialTextureSamplers(
1897
1347
  * feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): collect the
1898
1348
  * user-region texture handles for a material by iterating the shader's
1899
1349
  * `derive(paramSchema).textureFieldNames` SSOT (via
1900
- * `RenderAssetPort.materialShaderTextureFieldNames`). Each declared texture
1350
+ * `AssetRegistry.materialShaderTextureFieldNames`). Each declared texture
1901
1351
  * field whose paramValue resolves to a handle lands in the returned map keyed
1902
- * by field name; this is the single path through which any number of user-region
1352
+ * by field name; this is the single path through which an arbitrary number of user-region
1903
1353
  * textures (4 standard or 5+ custom, e.g. parallax `heightTexture`) flow.
1904
1354
  *
1905
- * When the effective schema is unavailable no user-region field is inferred.
1906
- * `emissiveTexture` / `occlusionTexture` are engine-injection (lightmap)
1907
- * textures and are NOT part of this set.
1355
+ * When the shader is not registered the built-in 4-field set is used so
1356
+ * standard materials still resolve. `emissiveTexture` / `occlusionTexture`
1357
+ * are engine-injection (lightmap) textures and are NOT part of this set.
1908
1358
  *
1909
1359
  * feat-20260623-world-space-video-asset M4 / w14 (D-5): a field whose paramValue
1910
1360
  * catalogues to a VideoAsset is routed into `videoOut` (a VideoAsset-branded
@@ -1918,8 +1368,7 @@ function collectUserRegionTextureHandles(
1918
1368
  pv: Readonly<Record<string, unknown>>,
1919
1369
  shaderId: string | undefined,
1920
1370
  paramSchema: readonly ParamSchemaEntry[] | undefined,
1921
- derivedInterface: DerivedMaterialInterface | undefined,
1922
- assetsRef: RenderAssetPort,
1371
+ assetsRef: AssetRegistry,
1923
1372
  world: World,
1924
1373
  resolveTex: (
1925
1374
  value: unknown,
@@ -1928,38 +1377,16 @@ function collectUserRegionTextureHandles(
1928
1377
  videoOut: Map<string, Handle<'VideoAsset', 'shared'>>,
1929
1378
  ): Map<string, Handle<'TextureAsset', 'shared'>> {
1930
1379
  const fields =
1931
- derivedInterface !== undefined
1932
- ? derivedInterface.textureFieldNames
1933
- : paramSchema !== undefined && paramSchema.length > 0
1380
+ materialTextureFields(
1381
+ shaderId,
1382
+ paramSchema !== undefined
1934
1383
  ? derive(paramSchema).textureFieldNames
1935
- : paramSchema !== undefined
1936
- ? new Set(
1937
- Object.entries(pv)
1938
- .filter(([, value]) => {
1939
- if (materialTextureValue(value) !== undefined) return true;
1940
- if (typeof value === 'number') {
1941
- const resolved = resolveRenderAsset<Asset>(
1942
- world,
1943
- value as Handle<string, 'shared'>,
1944
- );
1945
- return (
1946
- resolved.ok &&
1947
- (resolved.value.kind === 'texture' || resolved.value.kind === 'video')
1948
- );
1949
- }
1950
- if (typeof value === 'string') {
1951
- const payload = assetsRef.lookup(value);
1952
- return payload?.kind === 'texture' || payload?.kind === 'video';
1953
- }
1954
- return false;
1955
- })
1956
- .map(([field]) => field),
1957
- )
1958
- : shaderId !== undefined
1959
- ? assetsRef.materialShaderTextureFieldNames(shaderId)
1960
- : undefined;
1384
+ : shaderId !== undefined
1385
+ ? assetsRef.materialShaderTextureFieldNames(shaderId)
1386
+ : undefined,
1387
+ ) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
1961
1388
  const out = new Map<string, Handle<'TextureAsset', 'shared'>>();
1962
- for (const field of fields ?? []) {
1389
+ for (const field of fields) {
1963
1390
  // D-5: a video-kind paramValue is routed to the transient path (videoOut),
1964
1391
  // NOT minted as a TextureAsset (which would crash the record-stage
1965
1392
  // ensureResident, AC-08). A static field falls through to resolveTex.
@@ -1974,34 +1401,31 @@ function collectUserRegionTextureHandles(
1974
1401
  return out;
1975
1402
  }
1976
1403
 
1404
+ const ENGINE_INJECTED_TEXTURE_FIELDS = new Set(['emissiveTexture', 'occlusionTexture']);
1405
+
1406
+ function isEngineInjectedTextureField(shaderId: string | undefined, fieldName: string): boolean {
1407
+ return isStandardPbrMaterialShader(shaderId) && ENGINE_INJECTED_TEXTURE_FIELDS.has(fieldName);
1408
+ }
1409
+
1977
1410
  function materialParametersToParamSchema(
1978
1411
  parameters: readonly MaterialParameter[],
1979
1412
  shaderId: string | undefined,
1980
- assets: RenderAssetPort,
1981
1413
  ): readonly ParamSchemaEntry[] {
1982
- if (parameters.length === 0 && shaderId !== undefined) {
1983
- const shader = assets.shaderRegistry.findMaterialArtifact(shaderId);
1984
- if (shader.ok) return shader.value.paramSchema;
1985
- }
1414
+ const engineInjectedTextureFields = isStandardPbrMaterialShader(shaderId)
1415
+ ? ENGINE_INJECTED_TEXTURE_FIELDS
1416
+ : undefined;
1986
1417
  return parameters.flatMap((parameter): ParamSchemaEntry[] => {
1987
- // Static values select a cooked specialization and boolean material
1988
- // values have no runtime UBO representation in ParamSchemaEntry.
1989
- if (parameter.static || parameter.type === 'bool') return [];
1418
+ // Boolean material values are runtime-only and have no UBO representation.
1419
+ if (parameter.type === 'bool') return [];
1990
1420
  if (parameter.type === 'texture') {
1421
+ if (engineInjectedTextureFields?.has(parameter.name) === true) return [];
1991
1422
  return [{ name: parameter.name, type: 'texture2d' }];
1992
1423
  }
1993
- const defaultValue = parameter.default;
1994
- const numericDefault =
1995
- typeof defaultValue === 'number' ||
1996
- (Array.isArray(defaultValue) && defaultValue.every((item) => typeof item === 'number'))
1997
- ? { default: defaultValue }
1998
- : {};
1999
1424
  return [
2000
1425
  {
2001
1426
  name: parameter.name,
2002
1427
  type: parameter.type,
2003
1428
  ...(parameter.colorSpace === undefined ? {} : { colorSpace: parameter.colorSpace }),
2004
- ...numericDefault,
2005
1429
  },
2006
1430
  ];
2007
1431
  });
@@ -2016,7 +1440,7 @@ function materialParametersToParamSchema(
2016
1440
  function materialColorParameterSchema(
2017
1441
  parameters: readonly MaterialParameter[],
2018
1442
  shaderId: string | undefined,
2019
- assets: RenderAssetPort,
1443
+ assets: AssetRegistry,
2020
1444
  ): readonly MaterialColorParameterSchema[] {
2021
1445
  if (shaderId === undefined) return parameters;
2022
1446
  const shader = assets.shaderRegistry.findMaterialArtifact(shaderId);
@@ -2044,13 +1468,13 @@ function isDeepFrozen(value: unknown, seen = new Set<object>()): boolean {
2044
1468
 
2045
1469
  /**
2046
1470
  * Capture the object identities that make a resolved material stable. The
2047
- * renderer owns this cache, but the RenderAssetPort owns parent GUID lookup, so
1471
+ * renderer owns this cache, but the AssetRegistry owns parent GUID lookup, so
2048
1472
  * validation must walk the same chain on every cross-frame read. A malformed
2049
1473
  * or cyclic chain is deliberately not cacheable.
2050
1474
  */
2051
1475
  function captureMaterialCacheChain(
2052
1476
  root: MaterialAsset,
2053
- assets: RenderAssetPort,
1477
+ assets: AssetRegistry,
2054
1478
  ): readonly MaterialCacheChainIdentity[] | undefined {
2055
1479
  const chain: MaterialCacheChainIdentity[] = [];
2056
1480
  const visitedParentGuids = new Set<string>();
@@ -2063,7 +1487,7 @@ function captureMaterialCacheChain(
2063
1487
  if (parentRef === undefined) return undefined;
2064
1488
  if (visitedParentGuids.has(parentGuid)) return undefined;
2065
1489
  visitedParentGuids.add(parentGuid);
2066
- const parent = assets.lookup<Asset>(parentGuid);
1490
+ const parent = assets.lookup<Asset>(parentRef);
2067
1491
  if (parent?.kind !== 'material') return undefined;
2068
1492
  current = parent;
2069
1493
  }
@@ -2073,11 +1497,15 @@ function captureMaterialCacheChain(
2073
1497
  function readPersistentMaterialSnapshot(
2074
1498
  cache: MaterialSnapshotCache | undefined,
2075
1499
  handleRaw: number,
2076
- assets: RenderAssetPort,
1500
+ assets: AssetRegistry,
2077
1501
  ): MaterialSnapshotCacheEntry | undefined {
2078
1502
  const entry = cache?.get(handleRaw);
2079
1503
  if (entry === undefined) return undefined;
2080
- if (entry.crossFrameSafe && entry.chain !== undefined && entry.catalogEpoch === assets.epoch)
1504
+ if (
1505
+ entry.crossFrameSafe &&
1506
+ entry.chain !== undefined &&
1507
+ entry.catalogEpoch === assets.catalogEpoch
1508
+ )
2081
1509
  return entry;
2082
1510
  // The snapshot contains resolved texture and sampler handles as well as the
2083
1511
  // material parent chain. Any registry mutation can replace one of those
@@ -2096,12 +1524,12 @@ function readPersistentMaterialSnapshot(
2096
1524
  function readStablePersistentMaterialSnapshot(
2097
1525
  cache: MaterialSnapshotCache | undefined,
2098
1526
  handleRaw: number,
2099
- assets: RenderAssetPort,
1527
+ assets: AssetRegistry,
2100
1528
  ): MaterialSnapshotCacheEntry | undefined {
2101
1529
  const entry = cache?.get(handleRaw);
2102
1530
  return entry?.crossFrameSafe === true &&
2103
1531
  entry.chain !== undefined &&
2104
- entry.catalogEpoch === assets.epoch
1532
+ entry.catalogEpoch === assets.catalogEpoch
2105
1533
  ? entry
2106
1534
  : undefined;
2107
1535
  }
@@ -2112,7 +1540,7 @@ function storeMaterialSnapshot(
2112
1540
  snapshot: MaterialSnapshot,
2113
1541
  passes: readonly MaterialPass[],
2114
1542
  root: MaterialAsset,
2115
- assets: RenderAssetPort,
1543
+ assets: AssetRegistry,
2116
1544
  ): MaterialSnapshotCacheEntry {
2117
1545
  const chain = captureMaterialCacheChain(root, assets);
2118
1546
  const entry: MaterialSnapshotCacheEntry = {
@@ -2120,7 +1548,7 @@ function storeMaterialSnapshot(
2120
1548
  passes,
2121
1549
  crossFrameSafe: chain?.every(({ asset }) => isDeepFrozen(asset)) === true,
2122
1550
  ...(chain === undefined ? {} : { chain }),
2123
- ...(chain === undefined ? {} : { catalogEpoch: assets.epoch }),
1551
+ ...(chain === undefined ? {} : { catalogEpoch: assets.catalogEpoch }),
2124
1552
  };
2125
1553
  cache.set(handleRaw, entry);
2126
1554
  return entry;
@@ -2136,124 +1564,211 @@ function storeMaterialSnapshot(
2136
1564
  * UBO upload, so this helper produces a plain MaterialSnapshot only).
2137
1565
  *
2138
1566
  * Returns `defaultMaterialSnapshot()` (mid-grey unlit) for handle=0 (case-B
2139
- * sentinel, mirroring the inline path) and on any unresolved / non-material
1567
+ * sentinel, mirroring the inline path) and on each unresolved / non-material
2140
1568
  * asset (so a partially-mis-registered multi-material entity still renders
2141
1569
  * the resolvable submeshes; the count-mismatch validator already filtered
2142
1570
  * the count-disagreement case earlier).
2143
1571
  */
2144
- const materialInterfaceOwner = createMaterialInterfaceOwner({
2145
- defaultMaterialSnapshot,
2146
- readStablePersistentMaterialSnapshot,
2147
- readPersistentMaterialSnapshot,
2148
- storeMaterialSnapshot,
2149
- runtimeMaterialShaderId,
2150
- pipelineRenderState,
2151
- materialColorParameterSchema,
2152
- materialNormalScale,
2153
- materialParametersToParamSchema,
2154
- collectUserRegionTextureHandles,
2155
- collectMaterialTextureSamplers,
2156
- materialTextureRef,
2157
- collectMaterialTextureCoordinates,
2158
- internSharedRefFromGuid,
2159
- });
2160
-
2161
- export const resolveMaterialSnapshot = materialInterfaceOwner.resolveMaterialSnapshot;
2162
-
2163
- function materialPassTopologySignature(passes: readonly MaterialPass[]): string {
2164
- return JSON.stringify(
2165
- passes.map((pass) => {
2166
- const state = pass.renderState as
2167
- | (MaterialRenderState & {
2168
- readonly tags?: Readonly<Record<string, string>>;
2169
- readonly queue?: number;
2170
- readonly stencilReference?: number;
2171
- })
2172
- | undefined;
2173
- return {
2174
- name: pass.name,
2175
- module: pass.program.module,
2176
- vertexEntry: pass.program.vertexEntry,
2177
- fragmentEntry: pass.program.fragmentEntry,
2178
- moduleSlots: pass.program.moduleSlots,
2179
- queue: state?.queue,
2180
- tags: state?.tags,
2181
- renderState: pipelineRenderState(state),
2182
- stencilReference: state?.stencilReference,
2183
- };
2184
- }),
2185
- );
2186
- }
2187
-
2188
- export function refreshRenderableMaterialSnapshot(
2189
- renderable: RenderableSnapshot,
2190
- materialHandle: number,
1572
+ export function resolveMaterialSnapshot(
1573
+ handleRaw: number,
2191
1574
  world: World,
2192
- assets: RenderAssetPort,
2193
- gpuStore?: import('./gpu-resource-store').GpuResourceStore,
1575
+ assetsRef: AssetRegistry,
1576
+ materialSnapshotCache?: MaterialSnapshotCache,
2194
1577
  persistentMaterialSnapshotCache?: MaterialSnapshotCache,
2195
- paramSchemaProjectionCensus?: ParamSchemaProjectionCensus,
2196
- ): RenderableSnapshot | undefined {
2197
- const previousCacheEntry = persistentMaterialSnapshotCache?.get(materialHandle);
2198
- const previousPassTopology =
2199
- previousCacheEntry === undefined
2200
- ? undefined
2201
- : materialPassTopologySignature(previousCacheEntry.passes);
2202
- persistentMaterialSnapshotCache?.delete(materialHandle);
2203
- const refreshed = resolveMaterialSnapshot(
2204
- materialHandle,
2205
- world,
2206
- assets,
2207
- gpuStore,
2208
- new Map(),
1578
+ ): MaterialSnapshot {
1579
+ if (handleRaw === 0) return defaultMaterialSnapshot(handleRaw);
1580
+ const cached = materialSnapshotCache?.get(handleRaw);
1581
+ if (cached !== undefined) return cached.snapshot;
1582
+ const stablePersistentCached = readStablePersistentMaterialSnapshot(
2209
1583
  persistentMaterialSnapshotCache,
2210
- paramSchemaProjectionCensus,
1584
+ handleRaw,
1585
+ assetsRef,
2211
1586
  );
2212
- const materials = renderable.materials.map((material) =>
2213
- (material.materialHandle ?? 0) === materialHandle ? refreshed : material,
1587
+ if (stablePersistentCached !== undefined) {
1588
+ materialSnapshotCache?.set(handleRaw, stablePersistentCached);
1589
+ return stablePersistentCached.snapshot;
1590
+ }
1591
+ const tagged = toShared<'MaterialAsset'>(handleRaw);
1592
+ const res = resolveAssetHandle(world, tagged);
1593
+ if (!res.ok) return defaultMaterialSnapshot(handleRaw);
1594
+ const asset = res.value;
1595
+ if (asset.kind !== 'material') return defaultMaterialSnapshot(handleRaw);
1596
+ const persistentCached = readPersistentMaterialSnapshot(
1597
+ persistentMaterialSnapshotCache,
1598
+ handleRaw,
1599
+ assetsRef,
2214
1600
  );
2215
- if (persistentMaterialSnapshotCache !== undefined) {
2216
- const nextCacheEntry = persistentMaterialSnapshotCache.get(materialHandle);
2217
- if (
2218
- previousPassTopology === undefined ||
2219
- nextCacheEntry === undefined ||
2220
- materialPassTopologySignature(nextCacheEntry.passes) !== previousPassTopology
2221
- ) {
2222
- // A delta snapshot is insufficient to rebuild dispatch entries. If the
2223
- // resolved pass topology changed, let PersistentRenderScene rebuild the
2224
- // complete candidate domain so ShadowCaster membership and pass order
2225
- // come from the same extract owner.
2226
- return undefined;
1601
+ if (persistentCached !== undefined) {
1602
+ materialSnapshotCache?.set(handleRaw, persistentCached);
1603
+ return persistentCached.snapshot;
1604
+ }
1605
+ const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assetsRef);
1606
+ if (!resolvedResult.ok) return defaultMaterialSnapshot(handleRaw);
1607
+ const resolved = resolvedResult.value;
1608
+ const allPasses = resolved.passes;
1609
+ const firstPassShader =
1610
+ allPasses.length > 0
1611
+ ? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
1612
+ : undefined;
1613
+ const pv = materialValuesToLinearRuntime(
1614
+ resolved.values,
1615
+ materialColorParameterSchema(resolved.parameters ?? [], firstPassShader, assetsRef),
1616
+ resolved.colorSpace,
1617
+ ) as Readonly<Record<string, unknown>>;
1618
+ const baseColorPv = pv.baseColor as readonly number[] | undefined;
1619
+ const baseColor = vec3.create(
1620
+ baseColorPv?.[0] ?? 1,
1621
+ baseColorPv?.[1] ?? 1,
1622
+ baseColorPv?.[2] ?? 1,
1623
+ );
1624
+ const metallicPv = typeof pv.metallic === 'number' ? pv.metallic : 0;
1625
+ const roughnessPv = typeof pv.roughness === 'number' ? pv.roughness : 0.5;
1626
+ const clearcoatPv = typeof pv.clearcoat === 'number' ? pv.clearcoat : 0;
1627
+ const clearcoatRoughnessPv =
1628
+ typeof pv.clearcoatRoughness === 'number' ? pv.clearcoatRoughness : 0.5;
1629
+ const specularTintPv = pv.specularTint as readonly number[] | undefined;
1630
+ const normalScalePv = materialNormalScale(pv);
1631
+
1632
+ const paramSnap: Record<string, number | number[] | string> = {};
1633
+ for (const [k, v] of Object.entries(pv)) {
1634
+ if (typeof v === 'number') paramSnap[k] = v;
1635
+ else if (typeof v === 'string') paramSnap[k] = v;
1636
+ else if (Array.isArray(v) && v.every((x) => typeof x === 'number')) {
1637
+ paramSnap[k] = v as number[];
2227
1638
  }
2228
1639
  }
2229
- const topologyChanged = materials.some((material, index) => {
2230
- const previous = renderable.materials[index];
2231
- return (
2232
- previous === undefined ||
2233
- previous.shadowCaster !== material.shadowCaster ||
2234
- previous.transparent !== material.transparent ||
2235
- previous.materialShaderId !== material.materialShaderId ||
2236
- JSON.stringify(previous.renderState ?? null) !== JSON.stringify(material.renderState ?? null)
2237
- );
2238
- });
2239
- if (topologyChanged) return undefined;
2240
- const material = materials[0] ?? renderable.material;
2241
- const draws = renderable.gpuDrivenDraws;
2242
- return {
2243
- ...renderable,
2244
- material,
2245
- materials,
2246
- ...(draws === undefined
2247
- ? {}
2248
- : {
2249
- gpuDrivenDraws: draws.map((draw) => ({
2250
- ...draw,
2251
- materialResourceClass: gpuDrivenMaterialResourceClass(
2252
- materials[draw.materialSlot] ?? material,
2253
- ),
2254
- })),
2255
- }),
1640
+ const materialParamSchema = materialParametersToParamSchema(
1641
+ resolved.parameters ?? [],
1642
+ firstPassShader,
1643
+ );
1644
+ // feat-20260614 M8 (D-19): texture / sampler values are embedded GUIDs
1645
+ // (dash-form strings) after loadByGuid. Resolve each to a user-tier column
1646
+ // handle by looking up the catalogued payload and minting via
1647
+ // world.allocSharedRef; a numeric value (already a column handle from a
1648
+ // directly-minted material) passes through unchanged.
1649
+ const resolveTexLike = <B extends string>(
1650
+ value: unknown,
1651
+ brand: B,
1652
+ ): Handle<B, 'shared'> | undefined => {
1653
+ if (typeof value === 'number') return toShared<B>(value);
1654
+ if (typeof value === 'string') {
1655
+ // M4: intern the GUID -> column-handle resolution so each unique
1656
+ // (world, guid, brand) mints exactly ONE stable handle reused across
1657
+ // frames (stops the per-frame slot churn that defeated the GPU
1658
+ // residency cache). feat-20260619 M2 / w8: TextureAsset brand wires
1659
+ // onLastRelease -> gpuStore.evictTexture; other brands (SamplerAsset)
1660
+ // bypass (releaseUnreferenced-fallback lifecycle).
1661
+ return internSharedRefFromGuid(world, assetsRef, value, brand);
1662
+ }
1663
+ return undefined;
2256
1664
  };
1665
+ // feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): iterate the
1666
+ // shader's derive(paramSchema).textureFieldNames SSOT instead of a hardcoded
1667
+ // user-region field list, so an Nth texture (e.g. parallax heightTexture)
1668
+ // resolves through the same path. emissive/occlusion are engine-injection
1669
+ // (lightmap) textures, NOT in textureFieldNames, so they keep named reads.
1670
+ const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
1671
+ const textureHandles = collectUserRegionTextureHandles(
1672
+ pv,
1673
+ firstPassShader,
1674
+ materialParamSchema.length > 0 ? materialParamSchema : undefined,
1675
+ assetsRef,
1676
+ world,
1677
+ resolveTexLike,
1678
+ videoTextureFields,
1679
+ );
1680
+ const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
1681
+ resolveTexLike(materialTextureRef(value), 'SamplerAsset'),
1682
+ );
1683
+ const emissiveTextureHandle = resolveTexLike(
1684
+ materialTextureRef(pv.emissiveTexture),
1685
+ 'TextureAsset',
1686
+ );
1687
+ const occlusionTextureHandle = resolveTexLike(
1688
+ materialTextureRef(pv.occlusionTexture),
1689
+ 'TextureAsset',
1690
+ );
1691
+ const textureCoordinates = collectMaterialTextureCoordinates(pv);
1692
+ // Named user-region fields are derived from the map (sprite + legacy reads).
1693
+ const baseColorTextureHandle = textureHandles.get('baseColorTexture');
1694
+ const metallicRoughnessTextureHandle = textureHandles.get('metallicRoughnessTexture');
1695
+ const normalTextureHandle = textureHandles.get('normalTexture');
1696
+ const emissivePv = pv.emissive as readonly number[] | undefined;
1697
+ const snapshot: MaterialSnapshot = {
1698
+ baseColor,
1699
+ metallic: metallicPv,
1700
+ roughness: roughnessPv,
1701
+ clearcoat: clearcoatPv,
1702
+ clearcoatRoughness: clearcoatRoughnessPv,
1703
+ ...(specularTintPv !== undefined && {
1704
+ specularTint: [
1705
+ specularTintPv[0] ?? 1,
1706
+ specularTintPv[1] ?? 1,
1707
+ specularTintPv[2] ?? 1,
1708
+ ] as readonly [number, number, number],
1709
+ }),
1710
+ normalScale: normalScalePv,
1711
+ materialShaderId: firstPassShader,
1712
+ materialHandle: handleRaw,
1713
+ renderState: pipelineRenderState(allPasses[0]?.renderState),
1714
+ paramSnapshot: paramSnap,
1715
+ ...(materialParamSchema.length > 0 && { materialParamSchema }),
1716
+ ...(textureCoordinates.size > 0 && { textureCoordinates }),
1717
+ ...(textureHandles.size > 0 && { textureHandles }),
1718
+ ...(videoTextureFields.size > 0 && { videoTextureFields }),
1719
+ ...(samplerHandles.size > 0 && { samplerHandles }),
1720
+ ...(baseColorTextureHandle !== undefined && { baseColorTexture: baseColorTextureHandle }),
1721
+ ...(metallicRoughnessTextureHandle !== undefined && {
1722
+ metallicRoughnessTexture: metallicRoughnessTextureHandle,
1723
+ }),
1724
+ ...(normalTextureHandle !== undefined && { normalTexture: normalTextureHandle }),
1725
+ ...(emissivePv !== undefined && {
1726
+ emissive: [emissivePv[0] ?? 0, emissivePv[1] ?? 0, emissivePv[2] ?? 0] as readonly [
1727
+ number,
1728
+ number,
1729
+ number,
1730
+ ],
1731
+ }),
1732
+ ...(typeof pv.emissiveIntensity === 'number' && { emissiveIntensity: pv.emissiveIntensity }),
1733
+ ...(emissiveTextureHandle !== undefined && { emissiveTexture: emissiveTextureHandle }),
1734
+ ...(occlusionTextureHandle !== undefined && { occlusionTexture: occlusionTextureHandle }),
1735
+ ...(typeof pv.occlusionStrength === 'number' && { occlusionStrength: pv.occlusionStrength }),
1736
+ // feat-city-glb Bug 5 (per-submesh transparency): derive `transparent`
1737
+ // from the first pass's `renderState.blend` presence, identical to the
1738
+ // entity-level snapshot builder (extractFrame archetype loop). Without
1739
+ // this, per-submesh materials (materials[i>=1], e.g. a glTF BLEND decal
1740
+ // submesh on a multi-material mesh) never carry the transparent flag, so
1741
+ // the record stage's LDR split + blend routing treats them as opaque and
1742
+ // their alpha=0 texels composite as black.
1743
+ transparent: allPasses[0]?.renderState?.blend !== undefined,
1744
+ };
1745
+ const stored =
1746
+ materialSnapshotCache !== undefined
1747
+ ? storeMaterialSnapshot(
1748
+ materialSnapshotCache,
1749
+ handleRaw,
1750
+ snapshot,
1751
+ allPasses,
1752
+ asset,
1753
+ assetsRef,
1754
+ )
1755
+ : undefined;
1756
+ const cacheEntry =
1757
+ stored ??
1758
+ (persistentMaterialSnapshotCache !== undefined
1759
+ ? storeMaterialSnapshot(
1760
+ persistentMaterialSnapshotCache,
1761
+ handleRaw,
1762
+ snapshot,
1763
+ allPasses,
1764
+ asset,
1765
+ assetsRef,
1766
+ )
1767
+ : undefined);
1768
+ if (cacheEntry?.crossFrameSafe === true) {
1769
+ persistentMaterialSnapshotCache?.set(handleRaw, cacheEntry);
1770
+ }
1771
+ return snapshot;
2257
1772
  }
2258
1773
 
2259
1774
  /**
@@ -2269,6 +1784,132 @@ export interface ExtractPipelineSurface {
2269
1784
  readonly skinPaletteAllocator: SkinPaletteAllocator | null;
2270
1785
  }
2271
1786
 
1787
+ /**
1788
+ * PSSM (Parallel-Split Shadow Maps) split plane computation.
1789
+ *
1790
+ * Formula (plan-strategy D-8, research F1):
1791
+ * C_i = λ·n·(f/n)^(i/m) + (1-λ)·(n + i/m·(f-n))
1792
+ * where i = 1..m, n = near, f = far, m = cascadeCount
1793
+ *
1794
+ * The coverage range is [camera near, DirectionalLight.shadowDistance] — the
1795
+ * near end derives from the active camera, the far end is the shadowDistance
1796
+ * knob. Returns strictly monotonic view-space z depths (positive).
1797
+ *
1798
+ * When `far <= near + ε` (ε=1e-6), throws ShadowInvalidConfigError
1799
+ * (charter P3 explicit failure — research F2 guarantees formula stability
1800
+ * but degenerate configuration must be surfaced to the caller).
1801
+ *
1802
+ * @param nearPlane - near of the shadow casting range (from the camera near)
1803
+ * @param farPlane - far of the shadow casting range (DirectionalLight.shadowDistance)
1804
+ * @param cascadeCount - number of cascades (1..4)
1805
+ * @param splitLambda - PSSM split weight (0 = pure uniform, 1 = pure log)
1806
+ * @returns Array of m split depths (view-space z); length = cascadeCount,
1807
+ * guaranteed strictly monotonic, last element = far.
1808
+ *
1809
+ * @internal exported for w6/w7 testing only; production callers route through
1810
+ * extractFrame which validates cascadeCount/splitLambda via component schema.
1811
+ */
1812
+ /**
1813
+ * Compute the 8 world-space corner points of a camera frustum slice.
1814
+ *
1815
+ * Uses mat4.unproject to map NDC cube corners back to world space.
1816
+ * The slice is defined by view-space nearZ and farZ depths.
1817
+ *
1818
+ * For a WebGPU perspective matrix, ndcZ(viewZ) = camFar * (viewZ - camNear) /
1819
+ * (viewZ * (camFar - camNear)). We compute the ndcZ for each slice boundary
1820
+ * and unproject directly.
1821
+ *
1822
+ * @param vp - camera view-projection matrix (column-major 16 floats)
1823
+ * @param camNear - camera near plane (used to derive NDC mapping)
1824
+ * @param camFar - camera far plane (used to derive NDC mapping)
1825
+ * @param nearZ - near depth of the frustum slice (view-space, positive)
1826
+ * @param farZ - far depth of the frustum slice (view-space, positive)
1827
+ * @returns Array of 8 Vec3 world-space corner positions.
1828
+ */
1829
+ function computeFrustumCorners(
1830
+ vp: Mat4,
1831
+ camNear: number,
1832
+ camFar: number,
1833
+ nearZ: number,
1834
+ farZ: number,
1835
+ projection: 'perspective' | 'orthographic',
1836
+ ): Vec3[] {
1837
+ const invVP = mat4.create();
1838
+ mat4.invert(invVP, vp);
1839
+ const corners: Vec3[] = [];
1840
+
1841
+ // NDC z mapping is projection-dependent (WebGPU clip-space z in [0,1]):
1842
+ // perspective: ndc(z) = camFar * (z - camNear) / (z * (camFar - camNear))
1843
+ // orthographic: ndc(z) = (z - camNear) / (camFar - camNear)
1844
+ // feat-20260613-csm M6 / w22: orthographic cameras silently produced
1845
+ // garbage NDC z (perspective formula divides by viewZ, but ortho NDC is
1846
+ // linear), which mapped the cascade slab back to a degenerate world-space
1847
+ // corner set and the AABB-fit collapsed to near zero -- shadow_caster
1848
+ // wrote its triangles outside the [-1,1] clip volume so the depth
1849
+ // attachment stayed at clear=1.0 (root cause for shadow-m2 / shadow-m3 /
1850
+ // shadow-opt-out dawn red surfaced after w20's host-side ortho fix).
1851
+ const span = camFar - camNear;
1852
+ const ndcNear =
1853
+ projection === 'orthographic'
1854
+ ? (nearZ - camNear) / span
1855
+ : (camFar * (nearZ - camNear)) / (nearZ * span);
1856
+ const ndcFar =
1857
+ projection === 'orthographic'
1858
+ ? (farZ - camNear) / span
1859
+ : (camFar * (farZ - camNear)) / (farZ * span);
1860
+
1861
+ const signs = [-1, 1];
1862
+ for (const sx of signs) {
1863
+ for (const sy of signs) {
1864
+ corners.push(unprojectNDC(invVP, sx, sy, ndcNear));
1865
+ }
1866
+ }
1867
+ for (const sx of signs) {
1868
+ for (const sy of signs) {
1869
+ corners.push(unprojectNDC(invVP, sx, sy, ndcFar));
1870
+ }
1871
+ }
1872
+
1873
+ return corners;
1874
+ }
1875
+
1876
+ /**
1877
+ * Unproject a single NDC point to world space.
1878
+ */
1879
+ function unprojectNDC(invVP: Mat4, ndcX: number, ndcY: number, ndcZ: number): Vec3 {
1880
+ const ndc = vec3.create(ndcX, ndcY, ndcZ);
1881
+ const ws = vec3.create();
1882
+ mat4.unproject(ws, ndc, invVP);
1883
+ return ws;
1884
+ }
1885
+
1886
+ export function pssmSplit(
1887
+ nearPlane: number,
1888
+ farPlane: number,
1889
+ cascadeCount: number,
1890
+ splitLambda: number,
1891
+ ): Float32Array {
1892
+ const EPS = 1e-6;
1893
+ if (farPlane <= nearPlane + EPS) {
1894
+ throw new ShadowInvalidConfigError('shadowDistance', farPlane, nearPlane + EPS);
1895
+ }
1896
+
1897
+ const result = new Float32Array(cascadeCount);
1898
+ const m = cascadeCount;
1899
+ const n = nearPlane;
1900
+ const f = farPlane;
1901
+ const ratio = f / n;
1902
+
1903
+ for (let i = 1; i <= m; i++) {
1904
+ const t = i / m;
1905
+ const logPart = n * ratio ** t;
1906
+ const uniformPart = n + t * (f - n);
1907
+ result[i - 1] = splitLambda * logPart + (1 - splitLambda) * uniformPart;
1908
+ }
1909
+
1910
+ return result;
1911
+ }
1912
+
2272
1913
  // feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-5 (plan-strategy §D-3,
2273
1914
  // requirements §5.3): 6-face view-proj matrix table for one omnidirectional
2274
1915
  // shadow caster.
@@ -2342,8 +1983,196 @@ export function buildPointShadowMatrices(lightPos: Vec3, near: number, far: numb
2342
1983
  return out;
2343
1984
  }
2344
1985
 
2345
- // Directional CSM math is owned by extract/directional-csm.ts; this file keeps
2346
- // the historical internal export seam and feeds the owner with snapshots.
1986
+ /**
1987
+ * bug-20260710-editor-cross-world-shadow: the camera-frustum subset of
1988
+ * {@link CameraSnapshot} that the directional-CSM matrix builder consumes.
1989
+ * Extracted so the builder can run both inside {@link extractFrame} (single
1990
+ * world) AND at the {@link extractFrames} merge layer, where the camera and the
1991
+ * directional light may originate in DIFFERENT worlds (the editor
1992
+ * editorWorld/sceneWorld super-composite). A cameraless world produces zero
1993
+ * lightViewProj matrices, so the merge layer MUST recompute using the surfaced
1994
+ * camera — see {@link computeDirectionalCsm}.
1995
+ */
1996
+ export interface CsmCameraData {
1997
+ readonly world: Float32Array;
1998
+ readonly fov: number;
1999
+ readonly aspect: number;
2000
+ readonly near: number;
2001
+ readonly far: number;
2002
+ readonly projection: 'perspective' | 'orthographic';
2003
+ readonly orthoLeft: number;
2004
+ readonly orthoRight: number;
2005
+ readonly orthoBottom: number;
2006
+ readonly orthoTop: number;
2007
+ }
2008
+
2009
+ /**
2010
+ * bug-20260710-editor-cross-world-shadow: the raw directional-shadow config a
2011
+ * DirectionalLight carries, threaded onto {@link ExtractedLights} so the merge
2012
+ * layer can recompute CSM matrices against the surfaced camera. Distinct from
2013
+ * the *computed* outputs (lightViewProj / splitPlanes) also on ExtractedLights.
2014
+ */
2015
+ export interface DirectionalCsmConfig {
2016
+ readonly cascadeCount: number;
2017
+ readonly splitLambda: number;
2018
+ readonly cascadeBlend: number;
2019
+ readonly mapSize: number;
2020
+ readonly shadowDistance: number;
2021
+ }
2022
+
2023
+ /**
2024
+ * bug-20260710-editor-cross-world-shadow: computed directional-CSM output —
2025
+ * the per-cascade light-view-projection matrices plus the derived cascade
2026
+ * metadata. Returned by {@link computeDirectionalCsm}; `undefined` when the
2027
+ * config or camera is missing (caller leaves the fields undefined).
2028
+ */
2029
+ export interface DirectionalCsmResult {
2030
+ readonly lightViewProj: Float32Array[];
2031
+ readonly splitPlanes: Float32Array;
2032
+ readonly cascadeCount: number;
2033
+ readonly cascadeBlend: number;
2034
+ readonly shadowMapSize: number;
2035
+ }
2036
+
2037
+ /**
2038
+ * bug-20260710-editor-cross-world-shadow: pure directional-CSM matrix builder,
2039
+ * extracted verbatim from the former inline block in {@link extractFrame} (the
2040
+ * PSSM split + per-cascade frustum-slice AABB fit + orthographic light
2041
+ * projection). It is now a free function so BOTH the per-world extract and the
2042
+ * cross-world merge layer can call it — the merge layer is the only place that
2043
+ * pairs the first-hit directional light of one world with the surfaced camera
2044
+ * of another (editor super-composite). Returns `null` when there is no camera
2045
+ * to fit against (a cameraless world's directional light yields no matrices —
2046
+ * the caller leaves lightViewProj undefined rather than emitting zero matrices
2047
+ * that the WGSL reader would sample as NaN → "fully lit").
2048
+ *
2049
+ * Byte-identical to the prior inline computation for the single-world case
2050
+ * (same PSSM lambda, same toward-light Z reach RC-2 fix, same clip-space
2051
+ * matrix / tile-placement-in-shader split).
2052
+ */
2053
+ export function computeDirectionalCsm(
2054
+ direction: Vec3,
2055
+ config: DirectionalCsmConfig,
2056
+ cameraData: CsmCameraData | undefined,
2057
+ ): DirectionalCsmResult | null {
2058
+ const cascadeCount = Math.round(config.cascadeCount);
2059
+ // Coverage range: near from the active camera near (no separate near knob);
2060
+ // far is the component's shadowDistance. Fallback near (0.1) only when no
2061
+ // camera exists — but matrices are gated on cameraData below anyway.
2062
+ const sNear = cameraData?.near ?? 0.1;
2063
+ const sFar = config.shadowDistance;
2064
+
2065
+ // PSSM split planes: [camera near, shadowDistance], not the camera far.
2066
+ const splits = pssmSplit(sNear, sFar, cascadeCount, config.splitLambda);
2067
+ const paddedSplits = new Float32Array(4);
2068
+ for (let i = 0; i < splits.length; i++) paddedSplits[i] = splits[i] ?? 0;
2069
+
2070
+ // Light view matrix: camera at origin, looking along the light direction.
2071
+ const lightDirN = vec3.normalize(vec3.create(), direction);
2072
+ const lightTarget = vec3.create(lightDirN[0] ?? 0, lightDirN[1] ?? 0, lightDirN[2] ?? 0);
2073
+ const lightView = mat4.create();
2074
+ mat4.lookAt(lightView, vec3.create(0, 0, 0), lightTarget, vec3.create(0, 1, 0));
2075
+
2076
+ // Camera view-projection for frustum-corner computation. Without a camera the
2077
+ // frustum cannot be fit — bail so the caller emits no matrices (undefined),
2078
+ // NOT zero matrices (which read as NaN in the shadow shader → no shadow).
2079
+ if (cameraData === undefined) return null;
2080
+ const camProj = mat4.create();
2081
+ if (cameraData.projection === 'orthographic') {
2082
+ mat4.orthographic(
2083
+ camProj,
2084
+ cameraData.orthoLeft,
2085
+ cameraData.orthoRight,
2086
+ cameraData.orthoBottom,
2087
+ cameraData.orthoTop,
2088
+ cameraData.near,
2089
+ cameraData.far,
2090
+ );
2091
+ } else {
2092
+ mat4.perspective(camProj, cameraData.fov, cameraData.aspect, cameraData.near, cameraData.far);
2093
+ }
2094
+ const camView = mat4.create();
2095
+ mat4.invert(camView, cameraData.world);
2096
+ const cameraVP = mat4.create();
2097
+ mat4.multiply(cameraVP, camProj, camView);
2098
+
2099
+ const resultLightViewProjs: Float32Array[] = [];
2100
+
2101
+ // bug-20260619 RC-2 (AC-05): toward-light Z reach. Extend the near (toward-
2102
+ // light) bound of EVERY cascade to the toward-light extreme of the WHOLE
2103
+ // shadow frustum (sNear..sFar) so casters between the light and a slice are
2104
+ // admitted; X/Y stays per-cascade tight. Larger light-space z == closer to
2105
+ // the light (lookAt forward = eye-target), so the full-frustum max-z is the
2106
+ // toward-light reach used as -maxZ (the ortho near plane) per cascade.
2107
+ let lightSpaceMaxZFull = -Infinity;
2108
+ const fullCorners = computeFrustumCorners(
2109
+ cameraVP,
2110
+ cameraData.near,
2111
+ cameraData.far,
2112
+ sNear,
2113
+ sFar,
2114
+ cameraData.projection,
2115
+ );
2116
+ for (const ws of fullCorners) {
2117
+ const ls = vec3.create();
2118
+ mat4.transformVec3(ls, lightView, ws);
2119
+ if ((ls[2] ?? 0) > lightSpaceMaxZFull) lightSpaceMaxZFull = ls[2] ?? 0;
2120
+ }
2121
+
2122
+ // Pre-allocate 4-cascade array; fill in the effective cascades.
2123
+ for (let cIdx = 0; cIdx < 4; cIdx++) {
2124
+ if (cIdx >= cascadeCount) {
2125
+ resultLightViewProjs.push(new Float32Array(16));
2126
+ continue;
2127
+ }
2128
+ const cascadeNear = cIdx === 0 ? sNear : (splits[cIdx - 1] ?? sFar);
2129
+ const cascadeFar = splits[cIdx] ?? sFar;
2130
+ const corners = computeFrustumCorners(
2131
+ cameraVP,
2132
+ cameraData.near,
2133
+ cameraData.far,
2134
+ cascadeNear,
2135
+ cascadeFar,
2136
+ cameraData.projection,
2137
+ );
2138
+ const lightMVP = mat4.clone(lightView);
2139
+ let minX = Infinity;
2140
+ let maxX = -Infinity;
2141
+ let minY = Infinity;
2142
+ let maxY = -Infinity;
2143
+ let minZ = Infinity;
2144
+ let maxZ = -Infinity;
2145
+ for (const ws of corners) {
2146
+ const ls = vec3.create();
2147
+ mat4.transformVec3(ls, lightMVP, ws);
2148
+ if ((ls[0] ?? 0) < minX) minX = ls[0] ?? 0;
2149
+ if ((ls[0] ?? 0) > maxX) maxX = ls[0] ?? 0;
2150
+ if ((ls[1] ?? 0) < minY) minY = ls[1] ?? 0;
2151
+ if ((ls[1] ?? 0) > maxY) maxY = ls[1] ?? 0;
2152
+ if ((ls[2] ?? 0) < minZ) minZ = ls[2] ?? 0;
2153
+ if ((ls[2] ?? 0) > maxZ) maxZ = ls[2] ?? 0;
2154
+ }
2155
+ // Orthographic projection from light-space AABB. RC-2: whole-frustum
2156
+ // toward-light extreme for the near bound; per-cascade far (minZ) + X/Y.
2157
+ const nearZ = Math.max(maxZ, lightSpaceMaxZFull);
2158
+ const orthoProj = mat4.create();
2159
+ mat4.orthographic(orthoProj, minX, maxX, minY, maxY, -nearZ, -minZ);
2160
+ // lightViewProj = orthoProj * lightView — pure clip-space [-1,1]. Atlas
2161
+ // tile placement is handled by the per-cascade viewport + fragment-side UV
2162
+ // math (evalDirectional), so shadow_caster.gl_Position stays clip-space.
2163
+ void cIdx;
2164
+ resultLightViewProjs.push(new Float32Array(mat4.multiply(mat4.create(), orthoProj, lightView)));
2165
+ }
2166
+
2167
+ return {
2168
+ lightViewProj: resultLightViewProjs,
2169
+ splitPlanes: paddedSplits,
2170
+ cascadeCount,
2171
+ cascadeBlend: config.cascadeBlend,
2172
+ shadowMapSize: config.mapSize,
2173
+ };
2174
+ }
2175
+
2347
2176
  /**
2348
2177
  * feat-20260709-editor-world-partition M1 / w4 (AC-08, plan-strategy §2 D-3):
2349
2178
  * the owner index that previously served BOTH cameras and singleton render
@@ -2360,42 +2189,29 @@ export interface ExtractFramesOwner {
2360
2189
  }
2361
2190
 
2362
2191
  export interface PreparedExtractContext {
2363
- /** Renderer-local identity of the World whose singleton resources are read. */
2364
- readonly worldId?: number | undefined;
2365
- readonly assets: RenderAssetPort | null | undefined;
2192
+ readonly assets: AssetRegistry | null | undefined;
2366
2193
  readonly pipelineState: ExtractPipelineSurface | null | undefined;
2367
- readonly gpuStore: import('./gpu-resource-store').GpuResourceStore | undefined;
2368
2194
  readonly materialSnapshotCache: MaterialSnapshotCache | undefined;
2369
2195
  readonly cull: 'self' | 'none' | 'external';
2370
2196
  readonly cullCameras: readonly CameraSnapshot[] | undefined;
2371
2197
  readonly hierarchy: SceneHierarchySnapshot;
2372
- readonly visibility: VisibilitySnapshot;
2373
- /** @internal Debug-only ParamSchema compatibility census. */
2374
- readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus | undefined;
2375
- /** @internal Refresh only compact lighting resources over an existing frame. */
2376
- readonly resourceRefreshBase?: ExtractedFrame;
2377
- /**
2378
- * @internal Restrict the MeshRenderer row source to known entities. The
2379
- * query is addressed through Query.at(), so this path does not iterate the
2380
- * world's renderable tables. Resource queries remain unchanged.
2381
- */
2382
- readonly targetRenderableEntities?: readonly EntityHandle[];
2198
+ readonly visibility: VisibilitySnapshot;
2383
2199
  }
2384
2200
 
2385
2201
  export function extractCameraSnapshots(world: World): CameraSnapshot[] {
2202
+ const worldInternal = createWorldInternalView(world);
2386
2203
  const cameras: CameraSnapshot[] = [];
2387
2204
  const cameraEntities: number[] = [];
2388
- const cameraQuery = world.query({ read: [Camera, Transform] }).unwrap();
2205
+ const cameraQuery = world.query({ read: [Camera], with: [Transform] }).unwrap();
2389
2206
  for (const row of cameraQuery) {
2390
2207
  const cam = row.get(Camera);
2391
2208
  const entity = row.entity;
2392
- const transform = row.get(Transform);
2393
- if (transform === undefined) continue;
2394
- const worldMat = new Float32Array(transform.world);
2209
+ const view = worldInternal._getArrayView(entity, Transform, 'world');
2210
+ if (view === undefined) continue;
2211
+ const worldMat = new Float32Array(view);
2395
2212
  cameras.push({
2396
- worldId: 0,
2397
2213
  entityKey: entity as number,
2398
- position: mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like),
2214
+ position: mat4.getTranslation(vec3.create(), worldMat),
2399
2215
  world: worldMat,
2400
2216
  fov: cam.fov,
2401
2217
  aspect: cam.aspect,
@@ -2410,7 +2226,6 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
2410
2226
  exposure: cam.exposure,
2411
2227
  whitePoint: cam.whitePoint,
2412
2228
  antialias: antialiasFromF32(cam.antialias),
2413
- historyVersion: cam.historyVersion,
2414
2229
  bloom: bloomEnabledFromF32(cam.bloom),
2415
2230
  bloomThreshold: cam.bloomThreshold,
2416
2231
  bloomIntensity: cam.bloomIntensity,
@@ -2440,114 +2255,35 @@ function tonemapParams(camera: CameraSnapshot): Uint8Array {
2440
2255
  return new Uint8Array(bytes);
2441
2256
  }
2442
2257
 
2443
- function extractPostProcessParams(
2444
- world: World,
2445
- cameras: readonly CameraSnapshot[],
2446
- ): Map<string, Uint8Array> {
2447
- const postProcessParams = new Map<string, Uint8Array>();
2448
- const query = world.query({ with: [PostProcessParams] }).unwrap();
2449
- for (const row of query) {
2450
- const read = world.get(row.entity, PostProcessParams);
2451
- if (!read.ok) continue;
2452
- postProcessParams.set(read.value.shader, read.value.data);
2453
- }
2454
- const tonemapCamera = cameras[0];
2455
- if (tonemapCamera !== undefined) {
2456
- postProcessParams.set('forgeax::tonemap', tonemapParams(tonemapCamera));
2457
- }
2458
- return postProcessParams;
2459
- }
2460
-
2461
- export function refreshExtractedFrameCameras(world: World, frame: ExtractedFrame): ExtractedFrame {
2462
- const cameras = extractCameraSnapshots(world);
2463
- const camera = cameras[0];
2464
- const config = frame.lights.directionalCsmConfig;
2465
- const direction = frame.lights.directionalCsmDirection;
2466
- const csm =
2467
- camera === undefined || config === undefined || direction === undefined
2468
- ? null
2469
- : computeDirectionalCsm(direction, config, camera);
2470
- const postProcessParams = new Map(frame.postProcessParams);
2471
- postProcessParams.delete('forgeax::tonemap');
2472
- if (camera !== undefined) postProcessParams.set('forgeax::tonemap', tonemapParams(camera));
2473
- return {
2474
- ...frame,
2475
- cameras,
2476
- lights: {
2477
- ...frame.lights,
2478
- lightViewProj: csm?.lightViewProj,
2479
- splitPlanes: csm?.splitPlanes,
2480
- cascadeCount: csm?.cascadeCount,
2481
- cascadeBlend: csm?.cascadeBlend,
2482
- shadowMapSize: csm?.shadowMapSize,
2483
- },
2484
- postProcessParams,
2485
- };
2486
- }
2487
-
2488
- /**
2489
- * Refresh only the resource-owned PostProcessParams map after an ECS journal
2490
- * update. Post-process parameter entities are not render-domain slots; making
2491
- * their transient byte payload visible must not rebuild candidates, dispatch,
2492
- * topology, or the GPU scene.
2493
- */
2494
- export function refreshExtractedFramePostProcessParams(
2495
- world: World,
2496
- frame: ExtractedFrame,
2497
- ): ExtractedFrame {
2498
- const postProcessParams = new Map<string, Uint8Array>();
2499
- const query = world.query({ with: [PostProcessParams] }).unwrap();
2500
- for (const row of query) {
2501
- const read = world.get(row.entity, PostProcessParams);
2502
- if (!read.ok) continue;
2503
- postProcessParams.set(read.value.shader, read.value.data);
2504
- }
2505
- const tonemap = frame.postProcessParams.get('forgeax::tonemap');
2506
- if (tonemap !== undefined) postProcessParams.set('forgeax::tonemap', tonemap);
2507
- return { ...frame, postProcessParams };
2508
- }
2509
-
2510
2258
  export function prepareExtractContext(
2511
2259
  world: World,
2512
2260
  options: {
2513
- readonly worldId?: number;
2514
- readonly assets?: RenderAssetPort | null;
2261
+ readonly assets?: AssetRegistry | null;
2515
2262
  readonly pipelineState?: ExtractPipelineSurface | null;
2516
- readonly gpuStore?: import('./gpu-resource-store').GpuResourceStore;
2517
2263
  readonly materialSnapshotCache?: MaterialSnapshotCache;
2518
2264
  readonly cull?: 'self' | 'none' | 'external';
2519
2265
  readonly cullCameras?: readonly CameraSnapshot[];
2520
- readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus;
2521
2266
  } = {},
2522
2267
  ): PreparedExtractContext {
2523
2268
  const hierarchy = projectHierarchy(world);
2524
2269
  return {
2525
- worldId: options.worldId ?? 0,
2526
2270
  assets: options.assets,
2527
2271
  pipelineState: options.pipelineState,
2528
- gpuStore: options.gpuStore,
2529
2272
  materialSnapshotCache: options.materialSnapshotCache,
2530
2273
  cull: options.cull ?? 'self',
2531
2274
  cullCameras: options.cullCameras,
2532
2275
  hierarchy,
2533
2276
  visibility: resolveVisibility(world, hierarchy),
2534
- ...(options.paramSchemaProjectionCensus === undefined
2535
- ? {}
2536
- : { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
2537
2277
  };
2538
2278
  }
2539
2279
 
2540
2280
  export function extractFrames(
2541
2281
  worlds: readonly World[],
2542
2282
  owner: number | ExtractFramesOwner,
2543
- assets?: RenderAssetPort | null,
2283
+ assets?: AssetRegistry | null,
2544
2284
  pipelineState?: ExtractPipelineSurface | null,
2545
- gpuStore?: import('./gpu-resource-store').GpuResourceStore,
2546
2285
  materialSnapshotCachesByWorld?: MaterialSnapshotCachesByWorld,
2547
- options: {
2548
- readonly cull?: 'normal' | 'none';
2549
- readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus;
2550
- } = {},
2286
+ options: { readonly cull?: 'normal' | 'none' } = {},
2551
2287
  ): ExtractedFrame {
2552
2288
  // w4: normalize the owner argument. A bare number is the legacy single-owner
2553
2289
  // form (cameraOwner === resourceOwner); an object carries the two split
@@ -2559,7 +2295,7 @@ export function extractFrames(
2559
2295
  // ── D-2: frame-level side effects live here ────────────────────────────
2560
2296
  //
2561
2297
  // resetForFrame is called exactly once per frame, at the extractFrames
2562
- // entry. The skinPaletteAllocator cursor is reset before any per-world
2298
+ // entry. The skinPaletteAllocator cursor is reset before each per-world
2563
2299
  // extract runs, so sequential per-world allocation yields non-overlapping
2564
2300
  // palette slices (AC-08).
2565
2301
  const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
@@ -2567,55 +2303,7 @@ export function extractFrames(
2567
2303
  skinPaletteAllocator.resetForFrame();
2568
2304
  }
2569
2305
 
2570
- // The ordinary game path owns exactly one World. extractFrame already emits
2571
- // the canonical single-world shape: worldId=0, sorted dispatch, owner
2572
- // cameras/resources, visibility projections, and CSM matrices fitted to its
2573
- // own camera. Returning that snapshot directly avoids routing every frame
2574
- // through the multi-world reconciliation machinery below (ordering arrays,
2575
- // Maps, and per-renderable/dispatch object copies). This is an intra-frame
2576
- // structural fast path, not a cross-frame cache; every ECS query and derived
2577
- // snapshot is still refreshed on every call.
2578
- const onlyWorld = worlds.length === 1 ? worlds[0] : undefined;
2579
2306
  const failedWorlds = new Set<World>();
2580
- if (onlyWorld !== undefined && cameraOwner === 0 && resourceOwner === 0) {
2581
- try {
2582
- const prepared = prepareExtractContext(onlyWorld, {
2583
- ...(assets !== undefined ? { assets } : {}),
2584
- ...(pipelineState !== undefined ? { pipelineState } : {}),
2585
- ...(gpuStore !== undefined ? { gpuStore } : {}),
2586
- ...(materialSnapshotCachesByWorld === undefined
2587
- ? {}
2588
- : {
2589
- materialSnapshotCache:
2590
- materialSnapshotCachesByWorld.get(onlyWorld) ??
2591
- (() => {
2592
- const cache: MaterialSnapshotCache = new Map();
2593
- materialSnapshotCachesByWorld.set(onlyWorld, cache);
2594
- return cache;
2595
- })(),
2596
- }),
2597
- ...(options.cull === 'none' ? { cull: 'none' as const } : {}),
2598
- ...(options.paramSchemaProjectionCensus === undefined
2599
- ? {}
2600
- : { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
2601
- });
2602
- const frame = extractFrame(onlyWorld, prepared);
2603
- const fog = selectFog(onlyWorld, 0);
2604
- if (!fog.ok) {
2605
- routeFogSelectionError(onlyWorld, fog.error);
2606
- return frame;
2607
- }
2608
- return { ...frame, fog: fog.value.snapshot };
2609
- } catch (err) {
2610
- try {
2611
- reportRenderError(err);
2612
- } catch {
2613
- // The World error channel may throw under the default policy.
2614
- }
2615
- failedWorlds.add(onlyWorld);
2616
- }
2617
- }
2618
-
2619
2307
  // ── D-2: per-world extract with error isolation ────────────────────────
2620
2308
  //
2621
2309
  // Each world runs extractFrame over the final state published by
@@ -2644,10 +2332,8 @@ export function extractFrames(
2644
2332
  const isCameraOwner = wi === cameraOwner;
2645
2333
  const cameraOwnerFrame = framesByWorld.get(cameraOwner);
2646
2334
  const prepared = prepareExtractContext(world, {
2647
- worldId: wi,
2648
2335
  ...(assets !== undefined ? { assets } : {}),
2649
2336
  ...(pipelineState !== undefined ? { pipelineState } : {}),
2650
- ...(gpuStore !== undefined ? { gpuStore } : {}),
2651
2337
  ...(materialSnapshotCachesByWorld === undefined
2652
2338
  ? {}
2653
2339
  : {
@@ -2661,9 +2347,6 @@ export function extractFrames(
2661
2347
  }),
2662
2348
  cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
2663
2349
  ...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
2664
- ...(options.paramSchemaProjectionCensus === undefined
2665
- ? {}
2666
- : { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
2667
2350
  });
2668
2351
  const frame = extractFrame(world, prepared);
2669
2352
 
@@ -2671,7 +2354,10 @@ export function extractFrames(
2671
2354
  } catch (err) {
2672
2355
  // Per-world failure: route to world's own error handler, skip contribution.
2673
2356
  try {
2674
- reportRenderError(err);
2357
+ createWorldInternalView(world)._routeError(err, {
2358
+ severity: Severity.Error,
2359
+ systemName: `RenderSystem.extractFrames(world[${wi}])`,
2360
+ });
2675
2361
  } catch {
2676
2362
  // If _routeError itself throws, the world already failed — skip silently.
2677
2363
  }
@@ -2690,9 +2376,7 @@ export function extractFrames(
2690
2376
 
2691
2377
  // AC-04: renderables — concat by worlds[] order, stamp worldId.
2692
2378
  const renderables: RenderableSnapshot[] = [];
2693
- const candidateRenderables: RenderableSnapshot[] = [];
2694
2379
  const dispatchEntries: DispatchEntry[] = [];
2695
- const candidateDispatchEntries: DispatchEntry[] = [];
2696
2380
  const visibilitySnapshots: VisibilitySnapshot[] = [];
2697
2381
  const featureVisibilitySnapshots: RenderFeatureWorldVisibilitySnapshot[] = [];
2698
2382
  const hiddenEntityReports: RenderFeatureHiddenEntityReport[] = [];
@@ -2703,7 +2387,6 @@ export function extractFrames(
2703
2387
  if (f === undefined || wId === undefined) continue;
2704
2388
 
2705
2389
  const base = renderables.length;
2706
- const candidateBase = candidateRenderables.length;
2707
2390
  const visibilitySnapshot = f.visibilitySnapshots[0];
2708
2391
  const world = worlds[wId];
2709
2392
  if (visibilitySnapshot !== undefined) {
@@ -2716,18 +2399,7 @@ export function extractFrames(
2716
2399
  renderables.push({
2717
2400
  ...r,
2718
2401
  worldId: wId,
2719
- ...(r.candidateIndex !== undefined
2720
- ? { candidateIndex: candidateBase + r.candidateIndex }
2721
- : {}),
2722
- });
2723
- }
2724
- for (const r of f.candidateRenderables ?? f.renderables) {
2725
- candidateRenderables.push({
2726
- ...r,
2727
- worldId: wId,
2728
- ...(r.candidateIndex !== undefined
2729
- ? { candidateIndex: candidateBase + r.candidateIndex }
2730
- : {}),
2402
+ ...(r.pointsLines === undefined ? {} : { pointsLines: { ...r.pointsLines, worldId: wId } }),
2731
2403
  });
2732
2404
  }
2733
2405
 
@@ -2735,22 +2407,10 @@ export function extractFrames(
2735
2407
  for (const d of f.dispatch) {
2736
2408
  dispatchEntries.push({ ...d, renderableIndex: (d.renderableIndex ?? 0) + base });
2737
2409
  }
2738
- for (const d of f.candidateDispatch ?? f.dispatch) {
2739
- candidateDispatchEntries.push({
2740
- ...d,
2741
- renderableIndex: (d.renderableIndex ?? 0) + candidateBase,
2742
- });
2743
- }
2744
2410
  }
2745
2411
 
2746
2412
  // Stable sort dispatch by queue value.
2747
2413
  dispatchEntries.sort((a, b) => (a.queue ?? 0) - (b.queue ?? 0));
2748
- candidateDispatchEntries.sort((a, b) => (a.queue ?? 0) - (b.queue ?? 0));
2749
- const candidateDomain = buildRenderCandidateDomain(
2750
- candidateRenderables,
2751
- candidateDispatchEntries,
2752
- );
2753
- const candidateDispatchMembership = candidateDomain.membership;
2754
2414
 
2755
2415
  // AC-04: lights — point[]/spot[] concat; directional first-hit in
2756
2416
  // succeededFrames order (which preserves worlds[] order for successful
@@ -2767,7 +2427,6 @@ export function extractFrames(
2767
2427
  let depthBias: number | undefined;
2768
2428
  let normalBias: number | undefined;
2769
2429
  let pcfKernelSize: number | undefined;
2770
- let directionalCsm: DirectionalCsmResult | undefined;
2771
2430
  const pointShadow: PointShadowSnapshot[] = [];
2772
2431
  // bug-20260710-editor-cross-world-shadow: carry the raw CSM config +
2773
2432
  // direction of the first-hit directional so the merge layer can recompute
@@ -2807,10 +2466,7 @@ export function extractFrames(
2807
2466
  if (succeededIndices[fi] === cameraOwner) cameraOwnerFrame = succeededFrames[fi];
2808
2467
  if (succeededIndices[fi] === resourceOwner) resourceOwnerFrame = succeededFrames[fi];
2809
2468
  }
2810
- const cameras =
2811
- cameraOwnerFrame !== undefined
2812
- ? cameraOwnerFrame.cameras.map((camera) => ({ ...camera, worldId: cameraOwner }))
2813
- : [];
2469
+ const cameras = cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.cameras] : [];
2814
2470
 
2815
2471
  // bug-20260710-editor-cross-world-shadow: RECOMPUTE directional CSM matrices
2816
2472
  // against the surfaced camera. In a single-world app the per-world extract
@@ -2842,10 +2498,8 @@ export function extractFrames(
2842
2498
  directionalCsmDirection,
2843
2499
  directionalCsmConfig,
2844
2500
  mergeCameraData,
2845
- renderCandidateCsmCandidates(candidateDomain),
2846
2501
  );
2847
2502
  if (csm !== null) {
2848
- directionalCsm = csm;
2849
2503
  lightViewProj = csm.lightViewProj;
2850
2504
  splitPlanes = csm.splitPlanes;
2851
2505
  cascadeCount = csm.cascadeCount;
@@ -2872,35 +2526,30 @@ export function extractFrames(
2872
2526
  pointShadow,
2873
2527
  directionalCsmConfig,
2874
2528
  directionalCsmDirection,
2875
- ...(directionalCsm !== undefined ? { directionalCsm, cascades: directionalCsm.cascades } : {}),
2876
2529
  };
2877
- const environment =
2878
- resourceOwnerFrame?.environment ??
2879
- ({
2880
- kind: 'none',
2881
- worldId: resourceOwner,
2882
- skylightCount: 0,
2883
- skyboxCount: 0,
2884
- } satisfies ExtractedEnvironment);
2530
+ const skylight = resourceOwnerFrame?.skylight;
2531
+ const skylightCount = resourceOwnerFrame?.skylightCount ?? 0;
2532
+ const skybox = resourceOwnerFrame?.skybox;
2533
+ const skyboxCount = resourceOwnerFrame?.skyboxCount ?? 0;
2885
2534
  // User-authored PostProcessParams entities are singleton scene resources, so
2886
2535
  // they come from the resourceOwner world (holistic snapshot selection).
2887
2536
  const postProcessParams = new Map(resourceOwnerFrame?.postProcessParams);
2888
2537
  // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 1): the
2889
- // engine built-in `'forgeax::tonemap'` param is NOT a scene resource — the
2538
+ // engine built-in Standard tonemap param is NOT a scene resource — the
2890
2539
  // per-world extractFrame bridges it from that world's own `cameras[0]`
2891
2540
  // (Camera.exposure / whitePoint / tonemap is the SSOT). It therefore lives on
2892
2541
  // the CAMERA-owner frame, not the resource-owner frame. In the split-owner
2893
2542
  // editor topology the resourceOwner world has no Camera, so its frame carries
2894
- // no `forgeax::tonemap` entry; taking postProcessParams from resourceOwner
2543
+ // no Standard tonemap entry; taking postProcessParams from resourceOwner
2895
2544
  // alone drops it, the tonemap pass's params UBO stays zero-filled
2896
2545
  // (exposure=0 => tonemapped output is uniformly black), and the whole frame
2897
2546
  // reads black even though geometry drew into the HDR target. Overlay the
2898
2547
  // camera-owner frame's tonemap param (its SSOT source) so the surfaced
2899
2548
  // camera's exposure/whitePoint/mode reach the tonemap pass. When
2900
2549
  // cameraOwner === resourceOwner this is a no-op (identical entry). The 'na'
2901
- // literal 'forgeax::tonemap' mirrors the engine provider key set at the
2902
- // bottom of extractFrame (SSOT: same string, same 16B layout).
2903
- const TONEMAP_PARAM_KEY = 'forgeax::tonemap';
2550
+ // Standard tonemap identity mirrors the engine provider key set at the
2551
+ // bottom of extractFrame (SSOT: same identity, same 16B layout).
2552
+ const TONEMAP_PARAM_KEY = STANDARD_TONEMAP_FEATURE_ID;
2904
2553
  const cameraTonemapParam = cameraOwnerFrame?.postProcessParams.get(TONEMAP_PARAM_KEY);
2905
2554
  if (cameraTonemapParam !== undefined) {
2906
2555
  postProcessParams.set(TONEMAP_PARAM_KEY, cameraTonemapParam);
@@ -2920,21 +2569,16 @@ export function extractFrames(
2920
2569
  const visibilityStats = {
2921
2570
  explicitlyHidden: succeededFrames.reduce((s, f) => s + f.visibilityStats.explicitlyHidden, 0),
2922
2571
  };
2923
- const resourceWorld = worlds[resourceOwner];
2924
- const fog = selectFog(resourceWorld, resourceOwner);
2925
- if (!fog.ok) routeFogSelectionError(resourceWorld, fog.error);
2926
2572
 
2927
2573
  return {
2928
2574
  cameras,
2929
2575
  lights,
2930
2576
  renderables,
2931
2577
  dispatch: dispatchEntries,
2932
- candidateRenderables,
2933
- candidateDispatch: candidateDispatchEntries,
2934
- candidateDispatchMembership,
2935
- candidateDomain,
2936
- fog: fog.ok ? fog.value.snapshot : undefined,
2937
- environment,
2578
+ skylight,
2579
+ skylightCount,
2580
+ skybox,
2581
+ skyboxCount,
2938
2582
  frustumStats,
2939
2583
  visibilityStats,
2940
2584
  postProcessParams,
@@ -2982,6 +2626,36 @@ function hasFiniteOrderedLocalAabb(aabb: Float32Array | undefined): aabb is Floa
2982
2626
  return minX <= maxX && minY <= maxY && minZ <= maxZ;
2983
2627
  }
2984
2628
 
2629
+ function morphSnapshotFor(
2630
+ mesh: MeshAsset,
2631
+ weights: ArrayLike<number> | undefined,
2632
+ ): MorphSnapshot | undefined {
2633
+ const targets = mesh.morphTargets;
2634
+ if (targets === undefined || targets.length === 0 || weights === undefined) return undefined;
2635
+ if (weights.length !== targets.length) return undefined;
2636
+ const firstPositions = targets[0]?.position;
2637
+ if (
2638
+ firstPositions === undefined ||
2639
+ firstPositions.length === 0 ||
2640
+ firstPositions.length % 3 !== 0
2641
+ ) {
2642
+ return undefined;
2643
+ }
2644
+ const vertexCount = firstPositions.length / 3;
2645
+ if (mesh.vertices.length % vertexCount !== 0) return undefined;
2646
+ for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
2647
+ const target = targets[targetIndex];
2648
+ if (target?.position?.length !== firstPositions.length) return undefined;
2649
+ }
2650
+ const copied = new Float32Array(weights.length);
2651
+ for (let index = 0; index < weights.length; index += 1) {
2652
+ const weight = weights[index] ?? Number.NaN;
2653
+ if (!Number.isFinite(weight)) return undefined;
2654
+ copied[index] = weight;
2655
+ }
2656
+ return { weights: copied, targetCount: targets.length };
2657
+ }
2658
+
2985
2659
  export function extractFrame(world: World, context: PreparedExtractContext): ExtractedFrame {
2986
2660
  // feat-20260708-composited-multi-world-rendering M2 / D-2: resetForFrame
2987
2661
  // has been lifted to extractFrames (the frame-level entry point).
@@ -2990,16 +2664,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2990
2664
  const {
2991
2665
  assets,
2992
2666
  pipelineState,
2993
- gpuStore,
2994
2667
  materialSnapshotCache: persistentMaterialSnapshotCache,
2995
2668
  cull: cullMode,
2996
2669
  } = context;
2997
- const reportRenderError = (
2998
- error: unknown,
2999
- errorContext?: { readonly systemName: string },
3000
- ): void => {
3001
- routeWorldError(world, error, errorContext);
3002
- };
3003
2670
  const visibility = context.visibility.hasAnyHiddenIntent ? context.visibility : undefined;
3004
2671
  const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
3005
2672
 
@@ -3011,11 +2678,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3011
2678
  //
3012
2679
  // Each segment routes through a World-owned Query with explicit read and
3013
2680
  // optional roles. The packed entity handle for `readWorldMat4Copy` /
3014
- // row reads come from `row.entity` -- the
2681
+ // `_getArrayView` reads comes from `row.entity` -- the
3015
2682
  // archetype-graph back-door (`graph.archetypes` / `arch.components.some`) is gone.
3016
2683
  // Plan-decisions K-2 sniffing scheme B (archetype-edge sniff once via
3017
- // `row.get(X) !== undefined`); K-3 invariant preserved (row access
2684
+ // `row.get(X) !== undefined`); K-3 invariant preserved (`_getArrayView`
3018
2685
  // calls survive untouched, only the entity source changes).
2686
+ const worldInternal = createWorldInternalView(world);
2687
+
3019
2688
  const cameras = extractCameraSnapshots(world);
3020
2689
 
3021
2690
  // Three-query union (M2 / w16 / AC-03): directional has no Transform
@@ -3042,14 +2711,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3042
2711
  | undefined;
3043
2712
  for (const row of directionalLightQuery) {
3044
2713
  const l = row.get(DirectionalLight);
3045
- const validation = validateDirectionalLightData(
3046
- l as unknown as Readonly<Record<string, unknown>>,
3047
- );
3048
- if (!validation.ok) {
3049
- reportRenderError(validation.error, {
3050
- systemName: 'RenderSystem.extract (directional-light)',
3051
- });
3052
- }
3053
2714
  directionalCount += 1;
3054
2715
  const intensity = l.intensity;
3055
2716
  const snapshot: DirectionalLightSnapshot = {
@@ -3092,24 +2753,21 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3092
2753
  .unwrap();
3093
2754
  for (const row of pointLightQuery) {
3094
2755
  const p = row.get(PointLight);
3095
- const validation = validatePointLightData(p as unknown as Readonly<Record<string, unknown>>);
3096
- if (!validation.ok) {
3097
- reportRenderError(validation.error, { systemName: 'RenderSystem.extract (point-light)' });
3098
- }
3099
2756
  // K-2 scheme B: archetype-edge sniff -- `bundle.Transform` key is absent
3100
2757
  // when the archetype does not carry the Transform column.
3101
- const transform = row.get(Transform);
2758
+ const hasTransform = row.get(Transform) !== undefined;
3102
2759
  const intensity = p.intensity;
3103
2760
  const range = p.range;
3104
2761
  const entityId = row.entity;
3105
2762
  // Position = world-space translation extracted from Transform.world.
3106
2763
  // A point light archetype without a Transform column sits at the origin.
3107
2764
  let worldMat: Float32Array | undefined;
3108
- if (transform !== undefined) worldMat = new Float32Array(transform.world);
2765
+ if (hasTransform) {
2766
+ const view = worldInternal._getArrayView(entityId, Transform, 'world');
2767
+ if (view !== undefined) worldMat = new Float32Array(view);
2768
+ }
3109
2769
  const position =
3110
- worldMat !== undefined
3111
- ? mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like)
3112
- : vec3.create(0, 0, 0);
2770
+ worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
3113
2771
  pointSnapshots.push({
3114
2772
  kind: 'point',
3115
2773
  position,
@@ -3137,21 +2795,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3137
2795
  let spotTileNext = 0;
3138
2796
  for (const row of spotLightQuery) {
3139
2797
  const s = row.get(SpotLight);
3140
- const validation = validateSpotLightData(s as unknown as Readonly<Record<string, unknown>>);
3141
- if (!validation.ok) {
3142
- reportRenderError(validation.error, { systemName: 'RenderSystem.extract (spot-light)' });
3143
- }
3144
- const transform = row.get(Transform);
2798
+ const hasTransform = row.get(Transform) !== undefined;
3145
2799
  const intensity = s.intensity;
3146
2800
  const range = s.range;
3147
2801
  const innerConeDeg = s.innerConeDeg;
3148
2802
  const outerConeDeg = s.outerConeDeg;
3149
2803
  let worldMat: Float32Array | undefined;
3150
- if (transform !== undefined) worldMat = new Float32Array(transform.world);
2804
+ if (hasTransform) {
2805
+ const entity = row.entity;
2806
+ const view = worldInternal._getArrayView(entity, Transform, 'world');
2807
+ if (view !== undefined) worldMat = new Float32Array(view);
2808
+ }
3151
2809
  const position =
3152
- worldMat !== undefined
3153
- ? mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like)
3154
- : vec3.create(0, 0, 0);
2810
+ worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
3155
2811
  const dir = vec3.create(s.direction[0] ?? 0, s.direction[1] ?? -1, s.direction[2] ?? 0);
3156
2812
 
3157
2813
  // Extract is the single direction-normalization owner for direct-light
@@ -3161,6 +2817,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3161
2817
  );
3162
2818
  const EPSILON = 1e-6;
3163
2819
  const hasValidDirection = dirLen > EPSILON;
2820
+ if (!hasValidDirection) {
2821
+ worldInternal._routeError(
2822
+ new SpawnLightInvalidBoundsError('SpotLight', 'direction', [
2823
+ dir[0] ?? 0,
2824
+ dir[1] ?? 0,
2825
+ dir[2] ?? 0,
2826
+ ]),
2827
+ {
2828
+ severity: Severity.Error,
2829
+ systemName: 'RenderSystem.extract (spot-direction)',
2830
+ },
2831
+ );
2832
+ }
3164
2833
  const dirN = vec3.create(
3165
2834
  hasValidDirection ? (dir[0] ?? 0) / dirLen : (dir[0] ?? 0),
3166
2835
  hasValidDirection ? (dir[1] ?? 0) / dirLen : (dir[1] ?? 0),
@@ -3203,7 +2872,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3203
2872
 
3204
2873
  spotSnapshots.push({
3205
2874
  kind: 'spot',
3206
- entity: row.entity,
3207
2875
  // D-6: position reflects world transform; direction stays sourced
3208
2876
  // from SpotLight.direction (NOT rotated by the parent).
3209
2877
  position,
@@ -3276,7 +2944,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3276
2944
  cascadeBlend: sf.cascadeBlend,
3277
2945
  mapSize: sf.mapSize,
3278
2946
  shadowDistance: sf.shadowDistance,
3279
- pcfKernelSize: sf.pcfKernelSize,
3280
2947
  };
3281
2948
  directionalCsmDirection = dirSnapshot.direction;
3282
2949
  const csm = computeDirectionalCsm(dirSnapshot.direction, directionalCsmConfig, cameraData);
@@ -3327,14 +2994,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3327
2994
  for (const row of pointShadowQuery) {
3328
2995
  const t = row.get(Transform);
3329
2996
  const ps = row.get(PointLightShadow);
3330
- const validation = validatePointLightShadowData(
3331
- ps as unknown as Readonly<Record<string, unknown>>,
3332
- );
3333
- if (!validation.ok) {
3334
- reportRenderError(validation.error, {
3335
- systemName: 'RenderSystem.extract (point-light-shadow)',
3336
- });
3337
- }
3338
2997
  // Read world-space position from Transform.world (mat4 column-major;
3339
2998
  // translation lives at indices 12..14, mirroring CameraSnapshot.world
3340
2999
  // semantics in this file).
@@ -3400,7 +3059,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3400
3059
  }
3401
3060
  }
3402
3061
 
3403
- let lights: ExtractedLights = {
3062
+ const lights: ExtractedLights = {
3404
3063
  directional,
3405
3064
  directionalCount,
3406
3065
  point: pointSnapshots,
@@ -3485,56 +3144,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3485
3144
  }
3486
3145
  }
3487
3146
 
3488
- const environmentResult = selectEnvironmentSource({
3489
- resourceOwner: world,
3490
- worldId: context.worldId ?? 0,
3491
- skylight,
3492
- skylightCount,
3493
- skybox,
3494
- skyboxCount,
3495
- });
3496
- if (!environmentResult.ok) throw environmentResult.error;
3497
- const environment = environmentResult.value;
3498
-
3499
- const resourceRefreshBase = context.resourceRefreshBase;
3500
- if (resourceRefreshBase !== undefined) {
3501
- const candidateDomain =
3502
- resourceRefreshBase.candidateDomain ??
3503
- buildRenderCandidateDomain(
3504
- resourceRefreshBase.candidateRenderables ?? resourceRefreshBase.renderables,
3505
- resourceRefreshBase.candidateDispatch ?? resourceRefreshBase.dispatch,
3506
- );
3507
- const finalizedDirectionalCsm =
3508
- directionalCsmConfig !== undefined && directionalCsmDirection !== undefined
3509
- ? computeDirectionalCsm(
3510
- directionalCsmDirection,
3511
- directionalCsmConfig,
3512
- cameraData,
3513
- renderCandidateCsmCandidates(candidateDomain),
3514
- )
3515
- : undefined;
3516
- const refreshedLights =
3517
- finalizedDirectionalCsm === undefined || finalizedDirectionalCsm === null
3518
- ? lights
3519
- : {
3520
- ...lights,
3521
- lightViewProj: finalizedDirectionalCsm.lightViewProj,
3522
- splitPlanes: finalizedDirectionalCsm.splitPlanes,
3523
- cascadeCount: finalizedDirectionalCsm.cascadeCount,
3524
- cascadeBlend: finalizedDirectionalCsm.cascadeBlend,
3525
- shadowMapSize: finalizedDirectionalCsm.shadowMapSize,
3526
- directionalCsm: finalizedDirectionalCsm,
3527
- cascades: finalizedDirectionalCsm.cascades,
3528
- };
3529
- return {
3530
- ...resourceRefreshBase,
3531
- cameras,
3532
- lights: refreshedLights,
3533
- environment,
3534
- postProcessParams: extractPostProcessParams(world, cameras),
3535
- };
3536
- }
3537
-
3538
3147
  // feat-20260528-frustum-culling M3 / w10: precompute per-camera frustum
3539
3148
  // planes so entities can be tested against all cameras in the inner loop.
3540
3149
  // Cameras with degenerate projection parameters (e.g. zero fov, zero aspect)
@@ -3580,7 +3189,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3580
3189
  // carried in the world basis columns; the cull frustum uses the same view
3581
3190
  // the record stage derives, so cull stays same-source with render (AC-05).
3582
3191
  const view = mat4.create();
3583
- mat4.invert(view, cam.world as unknown as mat4.Mat4Like);
3192
+ mat4.invert(view, cam.world);
3584
3193
  const vp = mat4.create();
3585
3194
  mat4.multiply(vp, proj, view);
3586
3195
  const f = frustum.create();
@@ -3591,7 +3200,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3591
3200
 
3592
3201
  const renderables: RenderableSnapshot[] = [];
3593
3202
  // feat-20260528-frustum-culling M3 / w11: frustum culling counters.
3594
- const candidateRenderables: RenderableSnapshot[] = [];
3595
3203
  let frustumCulled = 0;
3596
3204
  let frustumTotal = 0;
3597
3205
  const explicitlyHidden = new Set<EntityHandle>();
@@ -3614,7 +3222,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3614
3222
  // (`row.get(X) !== undefined` edge sniff) replaces the prior
3615
3223
  // `arch.components.some` row-internal back-door. K-3 invariant: the
3616
3224
  // variable-length array reads (`MeshRenderer.materials`,
3617
- // `Instances.transforms`) still flow through row values /
3225
+ // `Instances.transforms`) still flow through `_getArrayView` /
3618
3226
  // `world.get(e, Instances)` -- only the `entity` source switches to
3619
3227
  // `row.entity`.
3620
3228
  //
@@ -3637,46 +3245,53 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3637
3245
  MeshFilter,
3638
3246
  Instances,
3639
3247
  Skin,
3640
- MorphWeights,
3641
3248
  Layer,
3249
+ MorphWeights,
3642
3250
  SpriteRegionOverride,
3643
3251
  SpriteInstances,
3252
+ Points,
3253
+ Lines,
3254
+ SortKey,
3644
3255
  ],
3645
3256
  })
3646
3257
  .unwrap();
3647
- // QueryRow deliberately exposes component data, not the matching
3648
- // archetype. Instance snapshots need that archetype's version for the
3649
- // record-stage GPU-buffer cache key, but ordinary renderables do not. The
3650
- // old path scanned every graph archetype for every matched query callback,
3651
- // even when the callback had no Instances/SpriteInstances column. Keep the
3652
- // lookup lazy: the common mesh-rendering path does no graph walk, and an
3653
- // instance-bearing frame builds one first-entity -> version index instead of
3654
- // repeating a full scan per matched archetype.
3655
- // Pending entries use the stable pre-main-cull candidate index. Persistent
3656
- // scene may later materialize a main/shadow union, so the visible array
3657
- // index is intentionally not used as the producer identity.
3658
- const candidateDispatch: DispatchEntry[] = [];
3258
+ // QueryRow deliberately exposes component data, not an archetype version.
3259
+ // The optimized ECS owner keeps table storage private, so instance-buffer
3260
+ // invalidation uses a content fingerprint of the managed arrays instead.
3261
+ const resolveArchVersion = (entity: EntityHandle): number => {
3262
+ const values = [
3263
+ worldInternal._getArrayView(entity, Instances, 'transforms'),
3264
+ worldInternal._getArrayView(entity, SpriteInstances, 'transforms'),
3265
+ worldInternal._getArrayView(entity, SpriteInstances, 'regions'),
3266
+ ];
3267
+ let hash = 2166136261;
3268
+ for (const value of values) {
3269
+ hash = Math.imul(hash ^ (value?.length ?? 0), 16777619) >>> 0;
3270
+ if (value === undefined) continue;
3271
+ for (let index = 0; index < value.length; index += 1) {
3272
+ hash = Math.imul(hash ^ Math.fround(value[index] ?? 0), 16777619) >>> 0;
3273
+ }
3274
+ }
3275
+ return hash;
3276
+ };
3277
+ // Pending entries are created with the current renderables.length, which is
3278
+ // exactly the slot this entity receives if it survives culling. No other
3279
+ // renderable can be pushed between staging and this entity's push, so publish
3280
+ // the original fresh entries instead of cloning every submesh descriptor.
3281
+ // A culled entity still discards its private pending array unchanged.
3659
3282
  const flushPendingDispatch = (pending: readonly DispatchEntry[]): void => {
3660
- for (const entry of pending) candidateDispatch.push(entry);
3283
+ for (const entry of pending) dispatch.push(entry);
3661
3284
  };
3662
- const meshRendererRows =
3663
- context.targetRenderableEntities === undefined
3664
- ? meshRendererQuery
3665
- : context.targetRenderableEntities.flatMap((entity) => {
3666
- const row = meshRendererQuery.at(entity);
3667
- return row === undefined ? [] : [row];
3668
- });
3669
- for (const row of meshRendererRows) {
3285
+ for (const row of meshRendererQuery) {
3670
3286
  // K-2 archetype-edge sniff (scheme B): a missing optional component
3671
3287
  // surfaces as an absent bundle key, not a row-internal optional chain.
3672
3288
  // Presence-only checks use QueryRow.has so large array-bearing components
3673
3289
  // are not materialised merely to answer a boolean question.
3674
- const transformData = row.get(Transform);
3675
- const hasTransform = transformData !== undefined;
3290
+ const hasTransform = row.has(Transform);
3676
3291
  const meshFilter = row.get(MeshFilter);
3677
3292
  const hasInstances = row.has(Instances);
3678
3293
  const skin = row.get(Skin);
3679
- const morphWeights = row.get(MorphWeights)?.weights;
3294
+ const morphWeightsView = worldInternal._getArrayView(row.entity, MorphWeights, 'weights');
3680
3295
  const hasMeshFilter = meshFilter !== undefined;
3681
3296
  const hasSkin = skin !== undefined;
3682
3297
  // feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 / w10:
@@ -3689,6 +3304,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3689
3304
  // - 'sprite-instances-count-mismatch'
3690
3305
  // (transforms.length / 16 !== regions.length / 4) — stride pair desync.
3691
3306
  const hasSpriteInstances = row.has(SpriteInstances);
3307
+ const points = row.get(Points);
3308
+ const lines = row.get(Lines);
3309
+ const sortKey = row.get(SortKey)?.value;
3310
+ const pointsLinesComponent =
3311
+ points !== undefined ? 'Points' : lines !== undefined ? 'Lines' : undefined;
3692
3312
  const isRenderable = hasTransform && hasMeshFilter;
3693
3313
 
3694
3314
  // feat-20260601 D-3: the resolved world transform is read per-entity from
@@ -3705,8 +3325,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3705
3325
  // feat-20260608-tilemap-object-layer-rendering M3 / m3-t5: tilemap-spawned
3706
3326
  // per-cell render entities (the ones `tilemap-chunk-extract-system`
3707
3327
  // pushes via `spawnDerivedRenderEntities`) reach this loop via the same
3708
- // archetype edge that carries any sprite entity -- they all wear
3709
- // a unit-quad MeshFilter + a `forgeax::sprite`-shaded
3328
+ // archetype edge that carries a sprite entity -- they all wear
3329
+ // `MeshFilter.assetHandle === HANDLE_QUAD` + a `forgeax::sprite`-shaded
3710
3330
  // material asset + the sprite-bucket `values.region` rectangle.
3711
3331
  // For the per-entity Y-sort path (requirements §AC-12 / §AC-13):
3712
3332
  //
@@ -3721,7 +3341,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3721
3341
  // bucket types therefore feed one `transparentSortEntries` argsort
3722
3342
  // step + share the `argsortInPlace` radix LSD primitive (plan-strategy
3723
3343
  // §D-1 / §D-3). The detection lives on the material side -- detect a
3724
- // tilemap-spawned entity by its unit-quad MeshFilter
3344
+ // tilemap-spawned entity by `MeshFilter.assetHandle === HANDLE_QUAD`
3725
3345
  // plus the `forgeax::sprite` shader id on `MeshRenderer.material`'s
3726
3346
  // first pass + non-empty `values.region`; no new public ECS
3727
3347
  // marker component lands (charter F1 minimum surface).
@@ -3739,10 +3359,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3739
3359
  // sprite reduces the anchor budget to 0.5 rather than the asset's 1.0.
3740
3360
  //
3741
3361
  // SpriteRegionOverride.region is a fixed stride-4 value. Per-row reads
3742
- // route through row values for the zero-copy row window
3362
+ // route through `_getArrayView` for the zero-copy row window
3743
3363
  // row-window slice (consistent with the variable-length array column
3744
- // reads -- K-3 carve-out keeps the row accessor as the owner of
3745
- // record for any non-scalar column).
3364
+ // reads -- K-3 carve-out keeps `_getArrayView` as the row-accessor of
3365
+ // record for each non-scalar column).
3746
3366
  const hasSpriteRegionOverride = row.has(SpriteRegionOverride);
3747
3367
  // feat-20260523-skin-skeleton-animation M2 / T-21: Skin component
3748
3368
  // column views for coexistence check + joint despawn fail-fast.
@@ -3754,11 +3374,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3754
3374
  if (hasInstances || hasSpriteInstances) {
3755
3375
  // All rows in this callback share one archetype. Only these two
3756
3376
  // instance-bearing paths consume the version in their cache key.
3757
- archVersion = 0;
3377
+ archVersion = resolveArchVersion(row.entity);
3758
3378
  }
3759
3379
 
3760
3380
  {
3761
- // feat-20260608 M2 / w11: read materials array via the component row
3381
+ // feat-20260608 M2 / w11: read materials array via _getArrayView
3762
3382
  const entity = row.entity;
3763
3383
  if (isRenderable && visibility?.effective(entity) === 'hidden') {
3764
3384
  explicitlyHidden.add(entity);
@@ -3777,9 +3397,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3777
3397
  // the deferred-shading smoke (PR #598 CI). Pure ordering fix; cull
3778
3398
  // logic and MeshRenderer contract unchanged.
3779
3399
  const pendingDispatch: DispatchEntry[] = [];
3780
- const candidateIndex = candidateRenderables.length;
3781
- const rendererValue = row.get(MeshRenderer);
3782
- const materialsView = rendererValue?.materials as unknown as Uint32Array | undefined;
3400
+ const materialsView = worldInternal._getArrayView(entity, MeshRenderer, 'materials') as
3401
+ | Uint32Array
3402
+ | undefined;
3783
3403
  const materialCount = materialsView?.length ?? 0;
3784
3404
  let materialHandles = Array.from(materialsView ?? []);
3785
3405
  let materialBindingSources: MeshMaterialBindingSource[] = materialHandles.map(
@@ -3798,7 +3418,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3798
3418
  assets !== null
3799
3419
  ) {
3800
3420
  const meshHandle = toShared<'MeshAsset'>(fAssetHandleVal);
3801
- const meshRes = resolveRenderAsset<Asset>(world, meshHandle);
3421
+ const meshRes = resolveAssetHandle<Asset>(world, meshHandle);
3802
3422
  if (meshRes.ok && meshRes.value.kind === 'mesh') {
3803
3423
  const meshAsset = meshRes.value as MeshAsset;
3804
3424
  gpuDrivenSubmeshes = meshAsset.submeshes;
@@ -3813,7 +3433,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3813
3433
  const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
3814
3434
  const resolvedBindings = resolveMeshMaterialBindings(meshAsset, materialsView ?? [], {
3815
3435
  isValidOverride(handle) {
3816
- const resolved = resolveRenderAsset<Asset>(world, toShared<'MaterialAsset'>(handle));
3436
+ const resolved = resolveAssetHandle(world, toShared<'MaterialAsset'>(handle));
3817
3437
  return resolved.ok && resolved.value.kind === 'material';
3818
3438
  },
3819
3439
  resolveMeshDefault(defaultGuid) {
@@ -3826,7 +3446,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3826
3446
  });
3827
3447
  if (!resolvedBindings.ok) {
3828
3448
  if (resolvedBindings.code === 'mesh-material-slots-missing') {
3829
- reportRenderError(
3449
+ worldInternal._routeError(
3830
3450
  new AssetError({
3831
3451
  code: 'load-failed',
3832
3452
  expected: 'every MeshAsset producer supplies materialSlots[]',
@@ -3837,19 +3457,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3837
3457
  subAssetGuid: '<material-slots-missing>',
3838
3458
  sourceField: { fieldName: 'materialSlots' },
3839
3459
  },
3840
- }) as unknown as Error,
3460
+ }),
3841
3461
  {
3462
+ severity: Severity.Error,
3842
3463
  systemName: 'RenderSystem.extract (mesh-material-slots-missing)',
3843
3464
  },
3844
3465
  );
3845
3466
  continue;
3846
3467
  }
3847
3468
  const materialGuid = AssetGuid.format(resolvedBindings.defaultMaterial);
3848
- reportRenderError(
3469
+ worldInternal._routeError(
3849
3470
  new AssetError({
3850
3471
  code: 'load-failed',
3851
3472
  expected: `MeshAsset materialSlots[${resolvedBindings.slotIndex}] default ${materialGuid} is ready and a MaterialAsset`,
3852
- hint: `AssetRegistry.load(meshGuid, meshAssetKind) must recursively load the declared default material; mesh=${guid}, slot=${resolvedBindings.slotIndex}, material=${materialGuid}`,
3473
+ hint: `loadByGuid(meshGuid) must recursively load the declared default material; mesh=${guid}, slot=${resolvedBindings.slotIndex}, material=${materialGuid}`,
3853
3474
  detail: {
3854
3475
  referencedByGuid: guid,
3855
3476
  referencedByKind: 'mesh',
@@ -3859,8 +3480,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3859
3480
  arrayIndex: resolvedBindings.slotIndex,
3860
3481
  },
3861
3482
  },
3862
- }) as unknown as Error,
3483
+ }),
3863
3484
  {
3485
+ severity: Severity.Error,
3864
3486
  systemName: 'RenderSystem.extract (mesh-default-not-ready)',
3865
3487
  },
3866
3488
  );
@@ -3884,7 +3506,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3884
3506
  },
3885
3507
  }));
3886
3508
  for (const diagnostic of resolvedBindings.diagnostics) {
3887
- reportRenderError(
3509
+ worldInternal._routeError(
3888
3510
  new AssetError({
3889
3511
  code: diagnostic.code,
3890
3512
  expected:
@@ -3904,8 +3526,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3904
3526
  slotIndex: diagnostic.slotIndex,
3905
3527
  handle: diagnostic.handle ?? 0,
3906
3528
  },
3907
- }) as unknown as Error,
3529
+ }),
3908
3530
  {
3531
+ severity: Severity.Warning,
3909
3532
  systemName: `RenderSystem.extract (${diagnostic.code})`,
3910
3533
  },
3911
3534
  );
@@ -3941,7 +3564,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3941
3564
  cachedMaterial.passes,
3942
3565
  entity,
3943
3566
  handleRaw,
3944
- candidateIndex,
3567
+ renderables.length,
3945
3568
  layerVal,
3946
3569
  materialSnap.paramSnapshot,
3947
3570
  );
@@ -3968,22 +3591,23 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3968
3591
  stablePersistentCached.passes,
3969
3592
  entity,
3970
3593
  handleRaw,
3971
- candidateIndex,
3594
+ renderables.length,
3972
3595
  layerVal,
3973
3596
  materialSnap.paramSnapshot,
3974
3597
  );
3975
3598
  }
3976
3599
  } else {
3977
- const res = resolveRenderAsset<Asset>(world, tagged);
3600
+ const res = resolveAssetHandle(world, tagged);
3978
3601
  if (!res.ok) {
3979
3602
  if (isRenderable) {
3980
3603
  const rhiErr = new RhiError({
3981
3604
  code: 'asset-not-registered',
3982
- expected: 'MeshRenderer.material in RenderAssetPort',
3605
+ expected: 'MeshRenderer.material in AssetRegistry',
3983
3606
  hint: 'catalog the material via assetRegistry.catalog(guid, asset) + world.allocSharedRef before spawn, or remove the material field to fall back to default',
3984
3607
  detail: { assetHandle: handleRaw },
3985
3608
  });
3986
- reportRenderError(rhiErr as unknown as Error, {
3609
+ worldInternal._routeError(rhiErr, {
3610
+ severity: Severity.Error,
3987
3611
  systemName: 'RenderSystem.extract (material asset-not-registered)',
3988
3612
  });
3989
3613
  }
@@ -3993,12 +3617,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3993
3617
  if (asset.kind !== 'material') {
3994
3618
  materialSnap = defaultMaterialSnapshot(handleRaw);
3995
3619
  } else {
3996
- const materialGuid = assets.guidOf(asset);
3997
3620
  // feat-20260529 M3 / w11: material parent chain inheritance via
3998
3621
  // read-through _materialWalk accessor (plan-strategy D-6).
3999
3622
  // The old direct asset.passes / asset.values read never
4000
3623
  // walked the parent chain, causing broken-inheritance (root cause).
4001
- const resolvedResult = resolveMaterialFromPort(asset, assets);
3624
+ const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assets);
4002
3625
  if (!resolvedResult.ok) {
4003
3626
  // AC-09 / S-7 / q8=A: passes-empty or cycle must fire structured
4004
3627
  // error through _routeError (same routing as asset-not-registered
@@ -4011,26 +3634,29 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4011
3634
  case 'material-value-unknown':
4012
3635
  case 'material-value-type-mismatch':
4013
3636
  case 'material-contract-program-mismatch':
4014
- reportRenderError(err as unknown as Error, {
3637
+ worldInternal._routeError(err, {
3638
+ severity: Severity.Error,
4015
3639
  systemName: `RenderSystem.extract (${err.code})`,
4016
3640
  });
4017
3641
  break;
4018
3642
  case 'material-circular-inheritance':
4019
- reportRenderError(err as unknown as Error, {
3643
+ worldInternal._routeError(err, {
3644
+ severity: Severity.Error,
4020
3645
  systemName: 'RenderSystem.extract (material-circular-inheritance)',
4021
3646
  });
4022
3647
  break;
4023
3648
  default:
4024
3649
  // Exhaustive guard: unhandled error codes from _materialWalk
4025
3650
  // surface an internal assertion to avoid silent continuation.
4026
- reportRenderError(err as unknown as Error, {
3651
+ worldInternal._routeError(err, {
3652
+ severity: Severity.Error,
4027
3653
  systemName: `RenderSystem.extract (_materialWalk: ${err.code})`,
4028
3654
  });
4029
3655
  }
4030
3656
  continue;
4031
3657
  }
4032
3658
  const resolved = resolvedResult.value;
4033
- const allPasses = resolved.passes ?? [];
3659
+ const allPasses = resolved.passes;
4034
3660
  const firstPassShader =
4035
3661
  allPasses.length > 0
4036
3662
  ? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
@@ -4064,50 +3690,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4064
3690
  }
4065
3691
  }
4066
3692
 
4067
- const registeredShader =
4068
- firstPassShader === undefined
4069
- ? undefined
4070
- : assets.shaderRegistry.findMaterialArtifact(firstPassShader);
4071
- const registeredProjection =
4072
- registeredShader?.ok === true
4073
- ? registeredShader.value.paramSchemaProjection
4074
- : undefined;
4075
- const materialParamSchema =
4076
- registeredProjection?.schema ??
4077
- materialParametersToParamSchema(resolved.parameters ?? [], firstPassShader, assets);
4078
- // An admitted empty schema is valid for vertex-only/shadow
4079
- // shaders, but older authored material payloads can still carry
4080
- // texture values while their manifest schema is empty. Keep that
4081
- // compatibility inference alive; a non-empty admitted schema is
4082
- // always the sole runtime projection owner.
4083
- const derivedMaterialInterface =
4084
- registeredProjection !== undefined && registeredProjection.schema.length > 0
4085
- ? registeredProjection.derivedInterface
4086
- : materialParamSchema.length > 0
4087
- ? deriveParamSchemaForCensus(
4088
- materialParamSchema,
4089
- context.paramSchemaProjectionCensus,
4090
- 'entity-extract',
4091
- )
4092
- : undefined;
4093
- if (
4094
- context.paramSchemaProjectionCensus !== undefined &&
4095
- registeredProjection !== undefined
4096
- ) {
4097
- context.paramSchemaProjectionCensus.record(
4098
- 'registered-projection-read',
4099
- 'entity-extract',
4100
- );
4101
- if (
4102
- registeredProjection.schema.length === 0 &&
4103
- Object.keys(resolved.values ?? {}).some((name) => name.endsWith('Texture'))
4104
- ) {
4105
- context.paramSchemaProjectionCensus.record(
4106
- 'empty-schema-legacy-inference',
4107
- 'entity-extract',
4108
- );
4109
- }
4110
- }
3693
+ const materialParamSchema = materialParametersToParamSchema(
3694
+ resolved.parameters ?? [],
3695
+ firstPassShader,
3696
+ );
4111
3697
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
4112
3698
  // bidirectional Skin <-> pbr-skin material fail-fast at extract.
4113
3699
  // Skin component without a forgeax::pbr-skin first-pass material
@@ -4126,20 +3712,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4126
3712
  skinSkeletonView !== 0;
4127
3713
  const isPbrSkinMaterial = firstPassShader === 'forgeax::pbr-skin';
4128
3714
  if (hasSkinSkel && !isPbrSkinMaterial) {
4129
- reportRenderError(
4130
- new SkinMaterialMismatchError(
4131
- entity as unknown as number,
4132
- firstPassShader,
4133
- ) as unknown as Error,
4134
- {
4135
- systemName: 'RenderSystem.extract (skin-material-mismatch)',
4136
- },
4137
- );
3715
+ worldInternal._routeError(new SkinMaterialMismatchError(entity, firstPassShader), {
3716
+ severity: Severity.Error,
3717
+ systemName: 'RenderSystem.extract (skin-material-mismatch)',
3718
+ });
4138
3719
  continue;
4139
3720
  }
4140
3721
  if (isPbrSkinMaterial && fAssetHandleVal !== undefined && fAssetHandleVal !== 0) {
4141
3722
  const meshHandleForSkinCheck = toShared<'MeshAsset'>(fAssetHandleVal);
4142
- const meshResForSkinCheck = resolveRenderAsset<MeshAsset>(
3723
+ const meshResForSkinCheck = resolveAssetHandle<MeshAsset>(
4143
3724
  world,
4144
3725
  meshHandleForSkinCheck,
4145
3726
  );
@@ -4150,15 +3731,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4150
3731
  if (!hasSkinIdx || !hasSkinWt) {
4151
3732
  const missing: 'skinIndex' | 'skinWeight' | 'both' =
4152
3733
  !hasSkinIdx && !hasSkinWt ? 'both' : !hasSkinIdx ? 'skinIndex' : 'skinWeight';
4153
- reportRenderError(
4154
- new MaterialSkinAttrMissingError(
4155
- entity as unknown as number,
4156
- missing,
4157
- ) as unknown as Error,
4158
- {
4159
- systemName: 'RenderSystem.extract (material-skin-attr-missing)',
4160
- },
4161
- );
3734
+ worldInternal._routeError(new MaterialSkinAttrMissingError(entity, missing), {
3735
+ severity: Severity.Error,
3736
+ systemName: 'RenderSystem.extract (material-skin-attr-missing)',
3737
+ });
4162
3738
  continue;
4163
3739
  }
4164
3740
  }
@@ -4183,32 +3759,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4183
3759
  const isSprite =
4184
3760
  firstPassShader === 'forgeax::sprite' || firstPassShader === 'forgeax::sprite-lit';
4185
3761
 
4186
- const emptySchemaTextureFields =
4187
- materialParamSchema.length === 0
4188
- ? new Set(
4189
- Object.entries(pv)
4190
- .filter(([, value]) => {
4191
- if (materialTextureValue(value) !== undefined) return true;
4192
- if (typeof value === 'number' && assets !== null && assets !== undefined) {
4193
- const resolved = resolveRenderAsset<Asset>(
4194
- world,
4195
- value as Handle<string, 'shared'>,
4196
- );
4197
- return (
4198
- resolved.ok &&
4199
- (resolved.value.kind === 'texture' || resolved.value.kind === 'video')
4200
- );
4201
- }
4202
- if (typeof value === 'string' && assets !== null && assets !== undefined) {
4203
- const payload = assets.lookup(value);
4204
- return payload?.kind === 'texture' || payload?.kind === 'video';
4205
- }
4206
- return false;
4207
- })
4208
- .map(([field]) => field),
4209
- )
4210
- : undefined;
4211
-
4212
3762
  // feat-20260613-material-paramschema-driven-binding M4 / w23
4213
3763
  // (D-5 graceful): paramSchema-driven texture-field validation.
4214
3764
  // For each handle-shaped paramValue (typeof === 'number'),
@@ -4232,7 +3782,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4232
3782
  if (assets === null || assets === undefined) return undefined;
4233
3783
  // M4: intern so the GUID mints one stable handle per World
4234
3784
  // instead of a fresh slot every frame (GPU residency relies on
4235
- // a stable handleSlot). Final release is owner evidence.
3785
+ // a stable handleSlot). onLastRelease -> gpuStore.evictTexture.
4236
3786
  const interned = internSharedRefFromGuid(
4237
3787
  world,
4238
3788
  assets,
@@ -4242,34 +3792,34 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4242
3792
  if (interned === undefined) return undefined;
4243
3793
  handle = interned;
4244
3794
  } else if (typeof textureRef === 'number') {
4245
- handle = textureRef as unknown as Handle<'TextureAsset', 'shared'>;
3795
+ handle = toShared<'TextureAsset'>(textureRef);
4246
3796
  } else {
4247
3797
  return undefined;
4248
3798
  }
4249
3799
  if (assets === null || assets === undefined) return handle;
4250
- const declaredFields =
4251
- derivedMaterialInterface !== undefined
4252
- ? derivedMaterialInterface.textureFieldNames
4253
- : (emptySchemaTextureFields ??
4254
- (firstPassShader !== undefined
4255
- ? assets.materialShaderTextureFieldNames(firstPassShader)
4256
- : undefined));
3800
+ const declaredFields = materialTextureFields(
3801
+ firstPassShader,
3802
+ materialParamSchema.length > 0
3803
+ ? derive(materialParamSchema).textureFieldNames
3804
+ : firstPassShader !== undefined
3805
+ ? assets.materialShaderTextureFieldNames(firstPassShader)
3806
+ : undefined,
3807
+ );
4257
3808
  // Shader not registered (R-4 cross-worktree path) -> trust the
4258
- // raw handle and let the record stage / GPU layer surface any
3809
+ // raw handle and let the record stage / GPU layer surface each
4259
3810
  // mismatch via MISSING_TEXTURE_HANDLE.
4260
3811
  if (declaredFields === undefined) return handle;
4261
3812
  // Field is not declared as a texture by the shader -> the
4262
3813
  // loader's "try every int" fallback misclassified a scalar;
4263
3814
  // drop the slot so the record stage uses the default white.
4264
- const declared =
4265
- declaredFields instanceof Set
4266
- ? declaredFields.has(fieldName)
4267
- : Array.from(declaredFields).includes(fieldName);
4268
- if (!declared) {
3815
+ if (
3816
+ !declaredFields.has(fieldName) &&
3817
+ !isEngineInjectedTextureField(firstPassShader, fieldName)
3818
+ ) {
4269
3819
  return undefined;
4270
3820
  }
4271
3821
  // Field declared as texture: verify the handle's asset kind.
4272
- const assetRes = resolveRenderAsset<Asset>(world, handle);
3822
+ const assetRes = resolveAssetHandle(world, handle);
4273
3823
  if (!assetRes.ok) return undefined;
4274
3824
  const kind = (assetRes.value as { kind?: string }).kind;
4275
3825
  if (kind !== 'texture') return undefined;
@@ -4283,7 +3833,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4283
3833
  brand: B,
4284
3834
  ): Handle<B, 'shared'> | undefined => {
4285
3835
  const value = materialTextureRef(raw);
4286
- if (typeof value === 'number') return value as unknown as Handle<B, 'shared'>;
3836
+ if (typeof value === 'number') return toShared<B>(value);
4287
3837
  const guid = assetReferenceText(value);
4288
3838
  if (guid !== undefined) {
4289
3839
  if (assets === null || assets === undefined) return undefined;
@@ -4299,15 +3849,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4299
3849
  // carried, replacing the hardcoded 3-field list. validateTextureHandle
4300
3850
  // already drops fields a shader doesn't declare as a texture.
4301
3851
  const userRegionFields =
4302
- derivedMaterialInterface !== undefined
4303
- ? derivedMaterialInterface.textureFieldNames
4304
- : (emptySchemaTextureFields ??
4305
- (firstPassShader !== undefined && assets !== null && assets !== undefined
3852
+ materialTextureFields(
3853
+ firstPassShader,
3854
+ materialParamSchema.length > 0
3855
+ ? derive(materialParamSchema).textureFieldNames
3856
+ : firstPassShader !== undefined && assets !== null && assets !== undefined
4306
3857
  ? assets.materialShaderTextureFieldNames(firstPassShader)
4307
- : undefined));
3858
+ : undefined,
3859
+ ) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
4308
3860
  const textureHandles = new Map<string, Handle<'TextureAsset', 'shared'>>();
4309
3861
  const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
4310
- for (const field of userRegionFields ?? []) {
3862
+ for (const field of userRegionFields) {
4311
3863
  // D-5: a video-kind paramValue routes to the transient path
4312
3864
  // (videoTextureFields), NOT validateTextureHandle (which drops
4313
3865
  // kind!=='texture', the R-7 silent-fail path). Static fields fall
@@ -4329,20 +3881,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4329
3881
  const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
4330
3882
  resolveParamHandle(materialTextureRef(value), 'SamplerAsset'),
4331
3883
  );
4332
- const emissiveTextureHandle = textureHandles.get('emissiveTexture');
4333
- const occlusionTextureHandle = textureHandles.get('occlusionTexture');
4334
- const textureCoordinates = collectMaterialTextureCoordinates(
4335
- pv,
4336
- materialParamSchema,
4337
- derivedMaterialInterface,
3884
+ const emissiveTextureHandle = validateTextureHandle(
3885
+ 'emissiveTexture',
3886
+ pv.emissiveTexture,
4338
3887
  );
4339
- const textureFacts = new Map<string, TextureAsset>();
4340
- for (const [name, handle] of textureHandles) {
4341
- const resolvedTexture = resolveRenderAsset<Asset>(world, handle);
4342
- if (resolvedTexture.ok && resolvedTexture.value.kind === 'texture') {
4343
- textureFacts.set(name, resolvedTexture.value);
4344
- }
4345
- }
3888
+ const occlusionTextureHandle = validateTextureHandle(
3889
+ 'occlusionTexture',
3890
+ pv.occlusionTexture,
3891
+ );
3892
+ const textureCoordinates = collectMaterialTextureCoordinates(pv);
4346
3893
  const emissivePv = pv.emissive as readonly number[] | undefined;
4347
3894
  // feat-20260625 M2 / w6: first-pass transparency flag folds into
4348
3895
  // MaterialSnapshot.transparent so the record stage can drive the
@@ -4369,10 +3916,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4369
3916
  // SpriteRegionOverride: per-entity per-frame region displacement.
4370
3917
  let overrideRegion: readonly [number, number, number, number] | undefined;
4371
3918
  if (hasSpriteRegionOverride) {
4372
- const overrideValue = world.get(entity, SpriteRegionOverride);
4373
- const overrideView = overrideValue.ok
4374
- ? (overrideValue.value.region as unknown as Float32Array)
4375
- : undefined;
3919
+ const overrideView = worldInternal._getArrayView(
3920
+ entity,
3921
+ SpriteRegionOverride,
3922
+ 'region',
3923
+ ) as Float32Array | undefined;
4376
3924
  if (overrideView !== undefined && overrideView.length >= 4) {
4377
3925
  overrideRegion = [
4378
3926
  overrideView[0] ?? 0,
@@ -4402,36 +3950,18 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4402
3950
  regionY += regionW;
4403
3951
  regionW = -regionW;
4404
3952
  }
4405
- paramSnap.region = [regionX, regionY, regionZ, regionW] as unknown as number[];
3953
+ paramSnap.region = [regionX, regionY, regionZ, regionW];
4406
3954
  // Guard: slicesAndMode must be present and zero for non-9-slice
4407
3955
  // sprites so the record-stage UBO writer (applyParamSnapshotToUbo)
4408
- // writes [0,0,0,0] rather than leaving unrelated derived payload
4409
- // values in that slot. A non-zero slicesAndMode trips
4410
- // `useSlices=true` in sprite.wgsl, which degenerates a four-vertex quad
4411
- // geometry -> invisible.
3956
+ // writes [0,0,0,0] at offset 48 instead of leaving the
3957
+ // buildPbrMaterialUboPayload PBR baseline (e.g. occlusionStrength=1
3958
+ // at that slot). A non-zero slicesAndMode trips `useSlices=true`
3959
+ // in sprite.wgsl, which degenerates HANDLE_QUAD geometry invisible.
4412
3960
  if (!('slicesAndMode' in paramSnap)) {
4413
3961
  (paramSnap as Record<string, unknown>).slicesAndMode = [0, 0, 0, 0];
4414
3962
  }
4415
3963
  }
4416
3964
 
4417
- // Sprite-specific region/slice values are folded into paramSnap
4418
- // above. Build the derived projection afterwards so the record
4419
- // stage writes those values through the same schema-owned payload
4420
- // as every other material parameter.
4421
- const derivedMaterialSlot =
4422
- materialParamSchema.length > 0
4423
- ? deriveMaterialSlotProjection({
4424
- material: String(handleRaw),
4425
- schema: materialParamSchema,
4426
- ...(derivedMaterialInterface === undefined
4427
- ? {}
4428
- : { derivedInterface: derivedMaterialInterface }),
4429
- values: { ...pv, ...paramSnap },
4430
- coordinates: textureCoordinates,
4431
- textures: textureFacts,
4432
- })
4433
- : undefined;
4434
-
4435
3965
  // Generic materialShaderId snapshot --- sprite included now flows
4436
3966
  // through this single branch (plan-strategy D-3 / AC-01 / AC-02 /
4437
3967
  // AC-07). The sprite block above only writes paramSnap.region (D-8
@@ -4452,18 +3982,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4452
3982
  }),
4453
3983
  normalScale: normalScalePv,
4454
3984
  materialShaderId: firstPassShader,
4455
- ...(materialGuid === undefined ? {} : { materialGuid }),
4456
3985
  materialHandle: handleRaw,
4457
- shadowCaster: hasShadowCasterPass(allPasses),
4458
3986
  renderState: pipelineRenderState(allPasses[0]?.renderState),
4459
3987
  paramSnapshot: paramSnap,
4460
3988
  ...(materialParamSchema.length > 0 && { materialParamSchema }),
4461
- ...(derivedMaterialSlot?.ok === true && {
4462
- derivedMaterialSlot: derivedMaterialSlot.value,
4463
- }),
4464
- ...(derivedMaterialSlot?.ok === false && {
4465
- derivedMaterialError: derivedMaterialSlot.error,
4466
- }),
4467
3989
  ...(textureCoordinates.size > 0 && { textureCoordinates }),
4468
3990
  ...(samplerHandles.size > 0 && { samplerHandles }),
4469
3991
  ...(textureHandles.size > 0 && { textureHandles }),
@@ -4516,7 +4038,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4516
4038
  allPasses,
4517
4039
  entity,
4518
4040
  handleRaw,
4519
- candidateIndex,
4041
+ renderables.length,
4520
4042
  layerVal,
4521
4043
  paramSnap,
4522
4044
  );
@@ -4550,7 +4072,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4550
4072
  // handleRaw===0 guard alone preserves the exact same dispatch shape.
4551
4073
  if (isRenderable && handleRaw === 0) {
4552
4074
  const shadowCasterTags: Record<string, string> = { LightMode: 'ShadowCaster' };
4553
- const nextRenderableIndex = candidateIndex;
4075
+ const nextRenderableIndex = renderables.length;
4554
4076
  // M2.5: stage into pendingDispatch; flushed at the renderable push
4555
4077
  // site below only when the entity survives cull.
4556
4078
  pendingDispatch.push({
@@ -4558,7 +4080,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4558
4080
  materialHandle: 0,
4559
4081
  renderableIndex: nextRenderableIndex,
4560
4082
  passIndex: 0,
4561
- queue: RenderQueue.Geometry,
4083
+ queue: 2000,
4562
4084
  layer: layerVal,
4563
4085
  tags: shadowCasterTags,
4564
4086
  renderState: undefined,
@@ -4570,22 +4092,16 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4570
4092
  });
4571
4093
  // Also add a Forward pass entry so the entity renders in the
4572
4094
  // main scene pass (mirrors Materials.unlit default).
4573
- const forwardTags: Record<string, string> = { LightMode: 'Forward' };
4574
- pendingDispatch.push({
4575
- entityIndex: entity,
4576
- materialHandle: 0,
4577
- renderableIndex: nextRenderableIndex,
4578
- passIndex: 1,
4579
- queue: RenderQueue.Geometry,
4580
- layer: layerVal,
4581
- tags: forwardTags,
4582
- renderState: undefined,
4583
- defines: undefined,
4584
- vertexEntry: 'vs_main',
4585
- fragmentEntry: 'fs_main',
4586
- materialShaderId: 'forgeax::default-unlit',
4587
- paramSnapshot: {},
4588
- });
4095
+ appendMaterialDispatchEntries(
4096
+ pendingDispatch,
4097
+ [DEFAULT_FORWARD_PASS],
4098
+ entity,
4099
+ 0,
4100
+ nextRenderableIndex,
4101
+ layerVal,
4102
+ {},
4103
+ 1,
4104
+ );
4589
4105
  }
4590
4106
 
4591
4107
  if (isRenderable) {
@@ -4605,30 +4121,21 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4605
4121
  ) {
4606
4122
  // Skin + Instances coexistence is forbidden (D-10).
4607
4123
  if (hasInstances) {
4608
- reportRenderError(
4609
- new SkinInstancesCoexistForbiddenError(
4610
- entity as unknown as number,
4611
- ) as unknown as Error,
4612
- {
4613
- systemName: 'RenderSystem.extract (skin-instances-coexist)',
4614
- },
4615
- );
4124
+ worldInternal._routeError(new SkinInstancesCoexistForbiddenError(entity), {
4125
+ severity: Severity.Error,
4126
+ systemName: 'RenderSystem.extract (skin-instances-coexist)',
4127
+ });
4616
4128
 
4617
4129
  continue;
4618
4130
  }
4619
4131
  // (a) Resolve skeleton asset; on failure -> skeleton-resolve-failed.
4620
4132
  const skeletonHandle = toShared<'SkeletonAsset'>(skeletonHandleRaw);
4621
- const skeletonRes = resolveRenderAsset<SkeletonAsset>(world, skeletonHandle);
4133
+ const skeletonRes = resolveAssetHandle<SkeletonAsset>(world, skeletonHandle);
4622
4134
  if (!skeletonRes.ok || skeletonRes.value.kind !== 'skeleton') {
4623
- reportRenderError(
4624
- new SkeletonResolveFailedError(
4625
- entity as unknown as number,
4626
- skeletonHandleRaw,
4627
- ) as unknown as Error,
4628
- {
4629
- systemName: 'RenderSystem.extract (skeleton-resolve-failed)',
4630
- },
4631
- );
4135
+ worldInternal._routeError(new SkeletonResolveFailedError(entity, skeletonHandleRaw), {
4136
+ severity: Severity.Error,
4137
+ systemName: 'RenderSystem.extract (skeleton-resolve-failed)',
4138
+ });
4632
4139
  continue;
4633
4140
  }
4634
4141
  const skeleton = skeletonRes.value;
@@ -4638,20 +4145,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4638
4145
  if (skinJoints === undefined) continue;
4639
4146
  const jointsLength = skinJoints.length;
4640
4147
  if (jointsLength !== skeleton.jointCount) {
4641
- reportRenderError(
4642
- new JointCountMismatchError(
4643
- entity as unknown as number,
4644
- skeleton.jointCount,
4645
- jointsLength,
4646
- ) as unknown as Error,
4148
+ worldInternal._routeError(
4149
+ new JointCountMismatchError(entity, skeleton.jointCount, jointsLength),
4647
4150
  {
4151
+ severity: Severity.Error,
4648
4152
  systemName: 'RenderSystem.extract (joint-count-mismatch)',
4649
4153
  },
4650
4154
  );
4651
4155
  continue;
4652
4156
  }
4653
4157
  // (c) Resolve only each joint's Transform.world column. The
4654
- // transient view is consumed before any structural mutation and
4158
+ // transient view is consumed before a structural mutation and
4655
4159
  // therefore preserves the same dangling-joint behavior without
4656
4160
  // constructing every other Transform field.
4657
4161
  // Build mat4 list eagerly so write happens once per entity (no
@@ -4660,27 +4164,24 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4660
4164
  let jointDangling = -1;
4661
4165
  for (let jIdx = 0; jIdx < skeleton.jointCount; jIdx++) {
4662
4166
  const jointEntityRaw = skinJoints[jIdx] ?? 0;
4663
- const jointEntity = jointEntityRaw as unknown as EntityHandle;
4664
- const jointTransform = world.get(jointEntity, Transform);
4665
- if (!jointTransform.ok) {
4167
+ const jointEntity = jointEntityRaw as EntityHandle;
4168
+ const jointWorld = worldInternal._getArrayView(jointEntity, Transform, 'world');
4169
+ if (jointWorld === undefined) {
4666
4170
  jointDangling = jIdx;
4667
4171
  break;
4668
4172
  }
4669
- // The view aliases the column-stored 16-float mat4 (column-major).
4670
- // Allocator's writeJointPalette expects a Mat4-shaped Float32Array.
4671
- // brand-cast-ok: reinterpret an existing storage view, no alloc.
4672
- jointWorlds[jIdx] = jointTransform.value.world as unknown as Mat4;
4173
+ // The allocator consumes the canonical Mat4 brand. Copy the
4174
+ // column view into that owner-created value so the ECS storage
4175
+ // view never crosses the math brand boundary by assertion.
4176
+ const jointWorldMat = mat4.create();
4177
+ jointWorldMat.set(jointWorld);
4178
+ jointWorlds[jIdx] = jointWorldMat;
4673
4179
  }
4674
4180
  if (jointDangling >= 0) {
4675
- reportRenderError(
4676
- new JointEntityDanglingError(
4677
- entity as unknown as number,
4678
- jointDangling,
4679
- ) as unknown as Error,
4680
- {
4681
- systemName: 'RenderSystem.extract (joint-entity-dangling)',
4682
- },
4683
- );
4181
+ worldInternal._routeError(new JointEntityDanglingError(entity, jointDangling), {
4182
+ severity: Severity.Error,
4183
+ systemName: 'RenderSystem.extract (joint-entity-dangling)',
4184
+ });
4684
4185
  continue;
4685
4186
  }
4686
4187
  // (d) Slice the IBM flat Float32Array into per-joint Float32Arrays.
@@ -4699,7 +4200,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4699
4200
  skinPaletteAllocator.writeJointPalette(slice, ibms, jointWorlds);
4700
4201
  skinSlice = {
4701
4202
  jointCount: slice.jointCount,
4702
- jointMatrices: new Float32Array(slice.jointMatrices),
4703
4203
  byteOffset: slice.byteOffset,
4704
4204
  buffer: slice.buffer,
4705
4205
  };
@@ -4712,14 +4212,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4712
4212
  // record stage copies these 16 floats into the mesh SSBO with zero
4713
4213
  // per-snapshot `mat4.compose` (AC-07). A stale slot (generation gone)
4714
4214
  // skips the renderable, mirroring the Instances dangling-row sweep.
4715
- // tweak-20260611 M1 / K-3: row access stays zero-copy;
4215
+ // tweak-20260611 M1 / K-3: `_getArrayView` call survives untouched;
4716
4216
  // only the `entity` source switched to `bundle.Entity.self[i]`.
4717
- if (transformData === undefined) continue;
4718
- const worldMat = new Float32Array(transformData.world);
4719
- const boundsWorld = authoredBoundsWorld(worldMat, transformData);
4720
- const candidateInstanceTransforms = hasInstances
4721
- ? (row.get(Instances)?.transforms as unknown as Float32Array | undefined)
4722
- : undefined;
4217
+ const worldView = worldInternal._getArrayView(entity, Transform, 'world');
4218
+ if (worldView === undefined) continue;
4219
+ const worldMat = new Float32Array(worldView);
4723
4220
  const transformSnap: TransformSnapshot = { world: worldMat };
4724
4221
  // feat-20260608 M5 amend / w11-a: per-submesh `materials[]` array
4725
4222
  // aligned 1-1 with `MeshAsset.submeshes[]`. materials[0] === the
@@ -4741,14 +4238,30 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4741
4238
  subHandle,
4742
4239
  world,
4743
4240
  assets,
4744
- gpuStore,
4745
4241
  materialSnapshotCache,
4746
4242
  persistentMaterialSnapshotCache,
4747
- context.paramSchemaProjectionCensus,
4748
4243
  ),
4749
4244
  );
4750
4245
  }
4751
4246
  }
4247
+ if (isRenderable && assets !== undefined && assets !== null) {
4248
+ for (let mi = 1; mi < materialHandles.length; mi++) {
4249
+ const subHandle = materialHandles[mi] ?? 0;
4250
+ if (subHandle === handleRaw) continue;
4251
+ const subEntry =
4252
+ materialSnapshotCache.get(subHandle) ??
4253
+ readPersistentMaterialSnapshot(persistentMaterialSnapshotCache, subHandle, assets);
4254
+ appendMaterialDispatchEntries(
4255
+ pendingDispatch,
4256
+ subEntry?.passes ?? (subHandle === 0 ? [DEFAULT_FORWARD_PASS] : []),
4257
+ entity,
4258
+ subHandle,
4259
+ renderables.length,
4260
+ layerVal,
4261
+ materialsArr[mi]?.paramSnapshot,
4262
+ );
4263
+ }
4264
+ }
4752
4265
 
4753
4266
  // feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 /
4754
4267
  // w10: SpriteInstances validation + snapshot materialisation.
@@ -4758,11 +4271,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4758
4271
  if (hasSpriteInstances) {
4759
4272
  // (1) mutually exclusive with Instances (peers — pick one).
4760
4273
  if (hasInstances) {
4761
- reportRenderError(
4762
- new SpriteInstancesMutuallyExclusiveWithInstancesError(
4763
- entity as unknown as number,
4764
- ) as unknown as Error,
4274
+ worldInternal._routeError(
4275
+ new SpriteInstancesMutuallyExclusiveWithInstancesError(entity),
4765
4276
  {
4277
+ severity: Severity.Error,
4766
4278
  systemName: 'RenderSystem.extract (sprite-instances-mutually-exclusive)',
4767
4279
  },
4768
4280
  );
@@ -4781,12 +4293,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4781
4293
  materialSnap.materialShaderId !== 'forgeax::sprite' &&
4782
4294
  materialSnap.materialShaderId !== 'forgeax::sprite-lit'
4783
4295
  ) {
4784
- reportRenderError(
4296
+ worldInternal._routeError(
4785
4297
  new SpriteInstancesRequiresSpriteShaderError(
4786
- entity as unknown as number,
4298
+ entity,
4787
4299
  materialSnap.materialShaderId ?? 'undefined',
4788
- ) as unknown as Error,
4300
+ ),
4789
4301
  {
4302
+ severity: Severity.Error,
4790
4303
  systemName: 'RenderSystem.extract (sprite-instances-requires-sprite-shader)',
4791
4304
  },
4792
4305
  );
@@ -4795,9 +4308,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4795
4308
  // (3) count mismatch — transforms.length / 16 === regions.length / 4
4796
4309
  // (transforms.length=0 + regions.length=0 is the zero-instance lawful
4797
4310
  // boundary; both derivations are 0 and equality holds, so no fire).
4798
- const spriteValue = row.get(SpriteInstances);
4799
- const transforms = spriteValue?.transforms as unknown as Float32Array | undefined;
4800
- const regions = spriteValue?.regions as unknown as Float32Array | undefined;
4311
+ const transforms = worldInternal._getArrayView(entity, SpriteInstances, 'transforms');
4312
+ const regions = worldInternal._getArrayView(entity, SpriteInstances, 'regions');
4801
4313
  if (transforms !== undefined && regions !== undefined) {
4802
4314
  const transformsLength = transforms.length;
4803
4315
  const regionsLength = regions.length;
@@ -4808,12 +4320,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4808
4320
  const tCount = transformsLength / 16;
4809
4321
  const rCount = regionsLength / 4;
4810
4322
  if (transformsLength % 16 !== 0 || regionsLength % 4 !== 0 || tCount !== rCount) {
4811
- reportRenderError(
4812
- new SpriteInstancesCountMismatchError(
4813
- transformsLength,
4814
- regionsLength,
4815
- ) as unknown as Error,
4323
+ worldInternal._routeError(
4324
+ new SpriteInstancesCountMismatchError(transformsLength, regionsLength),
4816
4325
  {
4326
+ severity: Severity.Error,
4817
4327
  systemName: 'RenderSystem.extract (sprite-instances-count-mismatch)',
4818
4328
  },
4819
4329
  );
@@ -4828,35 +4338,71 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4828
4338
  transforms: transformsCopy,
4829
4339
  regions: regionsCopy,
4830
4340
  instanceCount: tCount,
4831
- cacheKey: entity as unknown as number,
4341
+ cacheKey: entity,
4832
4342
  archVersion,
4833
4343
  };
4834
4344
  }
4835
4345
  }
4836
4346
 
4837
4347
  let localAabb: Float32Array | undefined;
4348
+ let morph: MorphSnapshot | undefined;
4838
4349
  const assetHandleRaw = Math.round(fAssetHandle ?? 0);
4839
4350
  if (assetHandleRaw !== 0) {
4840
- const meshRes = resolveRenderAsset<MeshAsset>(
4841
- world,
4842
- toShared<'MeshAsset'>(assetHandleRaw),
4843
- );
4844
- const meshAabb = meshRes.ok ? (meshRes.value as MeshAsset).aabb : undefined;
4845
- if (hasFiniteOrderedLocalAabb(meshAabb)) localAabb = new Float32Array(meshAabb);
4351
+ const meshRes = resolveAssetHandle(world, toShared<'MeshAsset'>(assetHandleRaw));
4352
+ if (meshRes.ok && meshRes.value.kind === 'mesh') {
4353
+ const meshAsset = meshRes.value as MeshAsset;
4354
+ morph = morphSnapshotFor(meshAsset, morphWeightsView);
4355
+ const meshAabb = meshAsset.aabb;
4356
+ // Morph targets can expand the authored bounds. Keep the
4357
+ // specialized lane conservative until a target-aware bounds
4358
+ // projection is available; a false-positive draw is preferable
4359
+ // to culling a valid deformed vertex.
4360
+ if (morph === undefined && hasFiniteOrderedLocalAabb(meshAabb)) {
4361
+ localAabb = new Float32Array(meshAabb);
4362
+ }
4363
+ }
4846
4364
  }
4847
4365
 
4366
+ const pointsLinesStyle: PointsLinesStyle | undefined =
4367
+ points !== undefined
4368
+ ? (() => {
4369
+ const shape = pointShapeFromU32(points.shape);
4370
+ return shape === undefined
4371
+ ? undefined
4372
+ : { kind: 'points' as const, sizePx: points.sizePx, shape };
4373
+ })()
4374
+ : lines === undefined
4375
+ ? undefined
4376
+ : { kind: 'lines' as const, widthPx: lines.widthPx };
4377
+ const cullingLocalAabb = expandPointsLinesBounds(localAabb ?? [], pointsLinesStyle);
4378
+ const pointsLines =
4379
+ pointsLinesComponent === undefined
4380
+ ? undefined
4381
+ : ({
4382
+ worldId: 0,
4383
+ entityKey: entity,
4384
+ component: pointsLinesComponent,
4385
+ meshHandle: assetHandleRaw,
4386
+ meshGeneration: assets?.catalogEpoch ?? 0,
4387
+ materialHandle: handleRaw,
4388
+ materialGeneration: assets?.catalogEpoch ?? 0,
4389
+ style: pointsLinesStyle,
4390
+ layer: layerVal,
4391
+ sortKey,
4392
+ visible: true,
4393
+ sourceBounds: cullingLocalAabb,
4394
+ viewport: { width: 0, height: 0, dpr: 1 },
4395
+ projection: identityProjection(),
4396
+ } satisfies PointsLinesRetainedSnapshot);
4397
+
4848
4398
  let nonIndexedFirst = 0;
4849
- const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh, submeshIndex) => {
4399
+ const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh) => {
4850
4400
  const drawMaterial = materialsArr[submesh.materialSlot] ?? materialSnap;
4851
- const forwardPass = gpuDrivenForwardPass(drawMaterial, materialSnapshotCache);
4852
4401
  const first = gpuDrivenIndexed ? submesh.indexOffset : nonIndexedFirst;
4853
4402
  nonIndexedFirst += submesh.vertexCount;
4854
- if (drawMaterial.transparent === true || forwardPass === undefined) return [];
4403
+ if (drawMaterial.transparent === true) return [];
4855
4404
  return [
4856
4405
  {
4857
- submeshIndex,
4858
- passIndex: forwardPass.passIndex,
4859
- queue: forwardPass.queue,
4860
4406
  kind: gpuDrivenIndexed ? ('indexed' as const) : ('non-indexed' as const),
4861
4407
  first,
4862
4408
  count: gpuDrivenIndexed ? submesh.indexCount : submesh.vertexCount,
@@ -4871,53 +4417,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4871
4417
  const baseRenderable: RenderableSnapshot = {
4872
4418
  assetHandle: Math.round(fAssetHandle ?? 0),
4873
4419
  transform: transformSnap,
4874
- ...(boundsWorld !== undefined ? { boundsWorld } : {}),
4875
- ...(candidateInstanceTransforms !== undefined
4876
- ? { candidateInstanceTransforms: new Float32Array(candidateInstanceTransforms) }
4877
- : {}),
4878
- ...(localAabb !== undefined ? { localAabb } : {}),
4420
+ ...(localAabb !== undefined ? { localAabb: cullingLocalAabb } : {}),
4879
4421
  material: materialSnap,
4880
4422
  materials: materialsArr,
4881
4423
  materialBindingSources,
4882
4424
  materialBindingDiagnostics,
4883
- ...(gpuDrivenDraws.length === 0 ? {} : { gpuDrivenDraws }),
4425
+ ...(morph === undefined && gpuDrivenDraws.length > 0 ? { gpuDrivenDraws } : {}),
4884
4426
  worldId: 0,
4885
- entityKey: entity as unknown as number,
4886
- candidateIndex,
4427
+ entityKey: entity,
4887
4428
  ...(skinSlice !== undefined ? { skin: skinSlice } : {}),
4888
- ...(morphWeights === undefined ? {} : { morphWeights: new Float32Array(morphWeights) }),
4429
+ ...(morph !== undefined ? { morph } : {}),
4889
4430
  ...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),
4431
+ ...(pointsLines !== undefined ? { pointsLines } : {}),
4890
4432
  };
4891
4433
 
4892
- let publishedRenderable = baseRenderable;
4893
- if (hasInstances) {
4894
- const instanceValue = row.get(Instances);
4895
- const transforms = instanceValue?.transforms as unknown as Float32Array | undefined;
4896
- if (transforms !== undefined) {
4897
- const actualLength = transforms.length;
4898
- if (actualLength % 16 !== 0) {
4899
- reportRenderError(new InstanceTransformsStrideMismatchError(actualLength), {
4900
- systemName: 'RenderSystem.extract (Instances stride)',
4901
- });
4902
-
4903
- continue;
4904
- }
4905
- publishedRenderable = {
4906
- ...baseRenderable,
4907
- instances: {
4908
- transforms: new Float32Array(transforms),
4909
- instanceCount: Math.max(1, Math.floor(actualLength / 16)),
4910
- cacheKey: entity as unknown as number,
4911
- archVersion,
4912
- },
4913
- };
4914
- }
4915
- }
4916
-
4917
- // Publish only the final, validated candidate snapshot. Its index is
4918
- // the current candidate-domain slot used by the staged dispatch.
4919
- candidateRenderables.push(publishedRenderable);
4920
-
4921
4434
  // feat-20260528-frustum-culling M3 / w10: frustum culling check.
4922
4435
  // Skip the entity if a valid AABB exists AND ALL cameras' frusta
4923
4436
  // reject the world-space AABB. Missing or malformed AABBs are
@@ -4929,13 +4442,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4929
4442
  // directly (no compose) -- same source the record stage feeds
4930
4443
  // the mesh SSBO, so cull stays same-source with render (AC-05).
4931
4444
  const worldAabb = box3.create();
4932
- box3.transformBox3(
4933
- worldAabb,
4934
- localAabb,
4935
- transformSnap.world as unknown as Parameters<typeof box3.transformBox3>[2],
4936
- );
4445
+ box3.transformBox3(worldAabb, cullingLocalAabb, transformSnap.world);
4937
4446
 
4938
- // Test against all cameras. Entity is visible if any camera
4447
+ // Test against all cameras. Entity is visible if one camera
4939
4448
  // frustum intersects the world-space AABB (or planes are empty
4940
4449
  // from degenerate projection).
4941
4450
  frustumTotal += 1;
@@ -4953,13 +4462,42 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4953
4462
  }
4954
4463
  if (!visible) {
4955
4464
  frustumCulled += 1;
4956
- for (const entry of pendingDispatch) candidateDispatch.push(entry);
4957
4465
  continue;
4958
4466
  }
4959
4467
  }
4960
4468
 
4961
- flushPendingDispatch(pendingDispatch);
4962
- renderables.push(publishedRenderable);
4469
+ if (hasInstances) {
4470
+ const transforms = worldInternal._getArrayView(entity, Instances, 'transforms');
4471
+ if (transforms === undefined) {
4472
+ flushPendingDispatch(pendingDispatch);
4473
+ renderables.push(baseRenderable);
4474
+ } else {
4475
+ const actualLength = transforms.length;
4476
+ if (actualLength % 16 !== 0) {
4477
+ worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
4478
+ severity: Severity.Error,
4479
+ systemName: 'RenderSystem.extract (Instances stride)',
4480
+ });
4481
+
4482
+ continue;
4483
+ }
4484
+ const snapshotCopy = new Float32Array(transforms);
4485
+ const instanceCount = Math.max(1, Math.floor(actualLength / 16));
4486
+ flushPendingDispatch(pendingDispatch);
4487
+ renderables.push({
4488
+ ...baseRenderable,
4489
+ instances: {
4490
+ transforms: snapshotCopy,
4491
+ instanceCount,
4492
+ cacheKey: entity,
4493
+ archVersion,
4494
+ },
4495
+ });
4496
+ }
4497
+ } else {
4498
+ flushPendingDispatch(pendingDispatch);
4499
+ renderables.push(baseRenderable);
4500
+ }
4963
4501
  }
4964
4502
 
4965
4503
  // feat-20260520-2d-sprite-layer-mvp M-3 / w22 + w25: finalise the
@@ -4973,53 +4511,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4973
4511
  }
4974
4512
  }
4975
4513
 
4976
- // M3 / w26: sort dispatch entries by queue (ascending, stable sort). The
4977
- // candidate dispatch stays pre-main-cull; the legacy dispatch is projected
4978
- // back to the visible subset for existing callers.
4979
- const visibleByCandidate = new Map<number, number>();
4980
- for (let index = 0; index < renderables.length; index += 1) {
4981
- const candidateIndex = renderables[index]?.candidateIndex;
4982
- if (candidateIndex !== undefined) visibleByCandidate.set(candidateIndex, index);
4983
- }
4984
- const sortedCandidateDispatch = sortDispatchByQueue(candidateDispatch);
4985
- const candidateDomain = buildRenderCandidateDomain(candidateRenderables, sortedCandidateDispatch);
4986
- const candidateDispatchMembership = candidateDomain.membership;
4987
- dispatch = sortDispatchByQueue(
4988
- sortedCandidateDispatch.flatMap((entry) => {
4989
- const renderableIndex = visibleByCandidate.get(entry.renderableIndex);
4990
- return renderableIndex === undefined ? [] : [{ ...entry, renderableIndex }];
4991
- }),
4992
- );
4993
- const finalizedDirectionalCsm =
4994
- directionalCsmConfig !== undefined && directionalCsmDirection !== undefined
4995
- ? computeDirectionalCsm(
4996
- directionalCsmDirection,
4997
- directionalCsmConfig,
4998
- cameraData,
4999
- renderCandidateCsmCandidates(candidateDomain),
5000
- )
5001
- : undefined;
5002
- if (finalizedDirectionalCsm !== null && finalizedDirectionalCsm !== undefined) {
5003
- lightViewProj = finalizedDirectionalCsm.lightViewProj;
5004
- splitPlanes = finalizedDirectionalCsm.splitPlanes;
5005
- cascadeCount = finalizedDirectionalCsm.cascadeCount;
5006
- cascadeBlend = finalizedDirectionalCsm.cascadeBlend;
5007
- shadowMapSize = finalizedDirectionalCsm.shadowMapSize;
5008
- }
5009
- lights = {
5010
- ...lights,
5011
- lightViewProj,
5012
- splitPlanes: splitPlanes !== undefined ? paddedSplitPlanes : undefined,
5013
- cascadeCount,
5014
- cascadeBlend,
5015
- shadowMapSize,
5016
- ...(finalizedDirectionalCsm !== null && finalizedDirectionalCsm !== undefined
5017
- ? {
5018
- directionalCsm: finalizedDirectionalCsm,
5019
- cascades: finalizedDirectionalCsm.cascades,
5020
- }
5021
- : {}),
5022
- };
4514
+ // M3 / w26: sort dispatch entries by queue (ascending, stable sort)
4515
+ // per plan-strategy D-3.
4516
+ dispatch = sortDispatchByQueue(dispatch);
5023
4517
 
5024
4518
  // D-1: collect PostProcessParams entities into Map<shaderId, Uint8Array>.
5025
4519
  // Last-one-wins when multiple entities bear the same shader id (mirrors
@@ -5037,17 +4531,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5037
4531
  // provider. The engine bridges the active camera's `Camera.exposure /
5038
4532
  // whitePoint / tonemap` onto the SAME unified params channel custom
5039
4533
  // post-processes use — `Camera.exposure` stays the AI-user-facing SSOT (D-5),
5040
- // the engine itself acts as the provider for the `'forgeax::tonemap'` shader
4534
+ // the engine itself acts as the provider for the Standard tonemap shader
5041
4535
  // id. The 16B layout is byte-identical to the prior recordTonemapPass packing
5042
4536
  // (render-system-record.ts pre-w14): Float32 [exposure, whitePoint, _, pad]
5043
4537
  // with the mode u32 occupying the third 4-byte slot via tonemapToU32 (SSOT in
5044
4538
  // camera.ts). Run AFTER the user-entity collection above so the engine's
5045
4539
  // built-in provider is authoritative for its own reserved key (a user entity
5046
- // can never shadow `'forgeax::tonemap'`). The single active camera mirrors
4540
+ // can never shadow the Standard tonemap identity. The single active camera mirrors
5047
4541
  // recordFrame's `activeCameras[0]` selection.
5048
4542
  const tonemapCamera = cameras[0];
5049
4543
  if (tonemapCamera !== undefined) {
5050
- postProcessParams.set('forgeax::tonemap', tonemapParams(tonemapCamera));
4544
+ postProcessParams.set(STANDARD_TONEMAP_FEATURE_ID, tonemapParams(tonemapCamera));
5051
4545
  }
5052
4546
 
5053
4547
  return {
@@ -5055,11 +4549,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5055
4549
  lights,
5056
4550
  renderables,
5057
4551
  dispatch,
5058
- candidateRenderables,
5059
- candidateDispatch: sortedCandidateDispatch,
5060
- candidateDispatchMembership,
5061
- candidateDomain,
5062
- environment,
4552
+ skylight,
4553
+ skylightCount,
4554
+ skybox,
4555
+ skyboxCount,
5063
4556
  frustumStats: { culled: frustumCulled, total: frustumTotal },
5064
4557
  visibilityStats: { explicitlyHidden: explicitlyHidden.size },
5065
4558
  postProcessParams,
@@ -5069,135 +4562,24 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5069
4562
  };
5070
4563
  }
5071
4564
 
5072
- /**
5073
- * Extract the complete render-domain facts for one known entity.
5074
- *
5075
- * This deliberately reuses extractFrame's MeshRenderer row body instead of
5076
- * maintaining a second material/skin/sprite implementation. Query.at() makes
5077
- * the row source identity-addressed; callers must supply a visibility snapshot
5078
- * appropriate for the current persistent owner and must keep hierarchy-affecting
5079
- * changes on the conservative rebuild lane.
5080
- */
5081
- export interface ExtractedRenderableEntity {
5082
- readonly snapshot: RenderableSnapshot;
5083
- readonly candidateDispatch: readonly DispatchEntry[];
5084
- }
5085
-
5086
- export function extractRenderableEntity(
5087
- world: World,
5088
- entity: EntityHandle,
5089
- context: PreparedExtractContext,
5090
- ): ExtractedRenderableEntity | undefined {
5091
- const frame = extractFrame(world, {
5092
- ...context,
5093
- cull: 'none',
5094
- cullCameras: undefined,
5095
- targetRenderableEntities: [entity],
5096
- });
5097
- const domain =
5098
- frame.candidateDomain ??
5099
- buildRenderCandidateDomain(
5100
- frame.candidateRenderables ?? frame.renderables,
5101
- frame.candidateDispatch ?? frame.dispatch,
5102
- );
5103
- const candidateIndex = domain.indexByIdentity.get(`0:${entity}`);
5104
- if (candidateIndex === undefined) return undefined;
5105
- const snapshot = domain.renderables[candidateIndex];
5106
- if (snapshot === undefined) return undefined;
5107
- return {
5108
- snapshot,
5109
- candidateDispatch: domain.dispatch.filter((entry) => entry.renderableIndex === candidateIndex),
5110
- };
5111
- }
5112
-
5113
- const RESOURCE_REFRESH_HIERARCHY: SceneHierarchySnapshot = {
5114
- parentOf: new Map(),
5115
- diagnostics: [],
5116
- getParent: () => undefined,
5117
- };
5118
-
5119
- const RESOURCE_REFRESH_VISIBILITY: VisibilitySnapshot = {
5120
- diagnostics: [],
5121
- hasAnyIntent: false,
5122
- hasAnyHiddenIntent: false,
5123
- get: () => undefined,
5124
- effective: () => 'visible',
5125
- };
5126
-
5127
- /**
5128
- * Refresh only light, shadow, sky, and post-process resources over an existing
5129
- * persistent frame. The resource path intentionally skips hierarchy,
5130
- * renderable, material, cull, dispatch, and candidate-domain extraction.
5131
- */
5132
- export function refreshExtractedFrameLights(world: World, frame: ExtractedFrame): ExtractedFrame {
5133
- const refreshed = extractFrame(world, {
5134
- assets: undefined,
5135
- pipelineState: undefined,
5136
- gpuStore: undefined,
5137
- materialSnapshotCache: undefined,
5138
- cull: 'none',
5139
- cullCameras: undefined,
5140
- hierarchy: RESOURCE_REFRESH_HIERARCHY,
5141
- visibility: frame.visibilitySnapshots[0] ?? RESOURCE_REFRESH_VISIBILITY,
5142
- resourceRefreshBase: frame,
5143
- });
5144
- const fog = selectFog(world, 0);
5145
- if (!fog.ok) {
5146
- routeFogSelectionError(world, fog.error);
5147
- return refreshed;
5148
- }
5149
- return { ...refreshed, fog: fog.value.snapshot };
5150
- }
5151
-
5152
- export function defaultTransformSnapshot(): TransformSnapshot {
5153
- // Identity world mat4 (column-major 16 floats).
5154
- return {
5155
- world: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
5156
- };
5157
- }
5158
-
5159
- // The default material path is hit once per renderable. Deriving its schema
5160
- // also hashes the layout, so retain the immutable projection instead of doing
5161
- // that work for every entity in a frame.
5162
- const DEFAULT_UNLIT_DERIVED_MATERIAL_SLOT = (() => {
5163
- const derived = deriveMaterialSlotProjection({
5164
- material: '0',
5165
- schema: DEFAULT_UNLIT_PARAM_SCHEMA,
5166
- values: { baseColor: [0.5, 0.5, 0.5, 1], alphaCutoff: 0 },
5167
- coordinates: defaultMaterialTextureCoordinates(DEFAULT_UNLIT_PARAM_SCHEMA),
5168
- });
5169
- if (!derived.ok) throw new Error('engine default material schema must derive');
5170
- return derived.value;
5171
- })();
5172
- const DEFAULT_MATERIAL_SNAPSHOT: MaterialSnapshot = Object.freeze({
5173
- baseColor: vec3.create(0.5, 0.5, 0.5),
5174
- metallic: 0,
5175
- roughness: 1,
5176
- materialHandle: 0,
5177
- materialParamSchema: DEFAULT_UNLIT_PARAM_SCHEMA,
5178
- derivedMaterialSlot: DEFAULT_UNLIT_DERIVED_MATERIAL_SLOT,
5179
- });
5180
-
5181
4565
  export function defaultMaterialSnapshot(materialHandle = 0): MaterialSnapshot {
5182
4566
  // Mid-grey unlit fallback (D-Q7 case B + extract-stage missing-spec),
5183
4567
  // matching the pre-w6 visual outcome where the record stage's force-cast
5184
4568
  // read of `firstMaterial.baseColorTexture` returned undefined and the
5185
4569
  // unlit fallback shader was selected.
5186
- const materialParamSchema = DEFAULT_UNLIT_PARAM_SCHEMA;
5187
- if (materialHandle === 0) return DEFAULT_MATERIAL_SNAPSHOT;
5188
- const derivedMaterialSlot = deriveMaterialSlotProjection({
5189
- material: String(materialHandle),
5190
- schema: materialParamSchema,
5191
- values: { baseColor: [0.5, 0.5, 0.5, 1], alphaCutoff: 0 },
5192
- coordinates: defaultMaterialTextureCoordinates(materialParamSchema),
5193
- }).unwrap();
5194
4570
  return {
5195
4571
  baseColor: vec3.create(0.5, 0.5, 0.5),
5196
4572
  metallic: 0,
5197
4573
  roughness: 1,
5198
4574
  materialHandle,
5199
- shadowCaster: true,
5200
- materialParamSchema,
5201
- derivedMaterialSlot,
5202
4575
  };
5203
4576
  }
4577
+
4578
+ function identityProjection(): Float32Array {
4579
+ const projection = new Float32Array(16);
4580
+ projection[0] = 1;
4581
+ projection[5] = 1;
4582
+ projection[10] = 1;
4583
+ projection[15] = 1;
4584
+ return projection;
4585
+ }