@forgeax/engine-render 0.1.3 → 0.1.6

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 (918) hide show
  1. package/README.md +130 -641
  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 +207 -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 +1 -2
  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 +10 -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-QC4VW3XJ.mjs +79 -0
  54. package/dist/chunk-QC4VW3XJ.mjs.map +1 -0
  55. package/dist/chunk-SCRSAK5B.mjs +480 -0
  56. package/dist/chunk-SCRSAK5B.mjs.map +1 -0
  57. package/dist/components/__tests__/schema-contract.unit.test.d.ts +2 -0
  58. package/dist/components/__tests__/schema-contract.unit.test.d.ts.map +1 -0
  59. package/dist/components/glyph-text.d.ts +1 -1
  60. package/dist/components/glyph-text.d.ts.map +1 -1
  61. package/dist/components/index.d.ts +2 -2
  62. package/dist/components/index.d.ts.map +1 -1
  63. package/dist/components/instances.d.ts +1 -1
  64. package/dist/components/instances.d.ts.map +1 -1
  65. package/dist/components/light-helpers.d.ts +1 -1
  66. package/dist/components/light-helpers.d.ts.map +1 -1
  67. package/dist/components/mesh-filter.d.ts +7 -7
  68. package/dist/components/mesh-filter.d.ts.map +1 -1
  69. package/dist/components/scene-instance.d.ts +11 -11
  70. package/dist/components/skybox-background.d.ts +56 -0
  71. package/dist/components/skybox-background.d.ts.map +1 -0
  72. package/dist/components/skylight.d.ts +55 -0
  73. package/dist/components/skylight.d.ts.map +1 -0
  74. package/dist/components/sprite-animation.d.ts +2 -3
  75. package/dist/components/sprite-animation.d.ts.map +1 -1
  76. package/dist/components/sprite-instances.d.ts +1 -1
  77. package/dist/components/sprite-playback-mode.d.ts +6 -1
  78. package/dist/components/sprite-playback-mode.d.ts.map +1 -1
  79. package/dist/components/sprite-region-override.d.ts +2 -3
  80. package/dist/components/sprite-region-override.d.ts.map +1 -1
  81. package/dist/components/tile-layer.d.ts +5 -0
  82. package/dist/components/tile-layer.d.ts.map +1 -1
  83. package/dist/construct-renderer.d.ts +18 -1
  84. package/dist/construct-renderer.d.ts.map +1 -1
  85. package/dist/construct-renderer.mjs +27008 -4
  86. package/dist/construct-renderer.mjs.map +1 -1
  87. package/dist/debug-draw-glue.d.ts +2 -53
  88. package/dist/debug-draw-glue.d.ts.map +1 -1
  89. package/dist/device/device-scope.d.ts +83 -0
  90. package/dist/device/device-scope.d.ts.map +1 -0
  91. package/dist/{gpu-resource-store.d.ts → device/gpu-residency.d.ts} +91 -48
  92. package/dist/device/gpu-residency.d.ts.map +1 -0
  93. package/dist/device/resource-types.d.ts +8 -0
  94. package/dist/device/resource-types.d.ts.map +1 -0
  95. package/dist/engine-metrics.d.ts +2 -2
  96. package/dist/engine-metrics.d.ts.map +1 -1
  97. package/dist/errors/recover.d.ts +28 -1
  98. package/dist/errors/recover.d.ts.map +1 -1
  99. package/dist/errors/render.d.ts +249 -258
  100. package/dist/errors/render.d.ts.map +1 -1
  101. package/dist/features/fullscreen.d.ts +19 -0
  102. package/dist/features/fullscreen.d.ts.map +1 -0
  103. package/dist/features/host.d.ts +32 -26
  104. package/dist/features/host.d.ts.map +1 -1
  105. package/dist/features/morph/morph-feature.d.ts +39 -5
  106. package/dist/features/morph/morph-feature.d.ts.map +1 -1
  107. package/dist/features/plan.d.ts +182 -0
  108. package/dist/features/plan.d.ts.map +1 -0
  109. package/dist/features/prepared-gpu-work.d.ts +21 -17
  110. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  111. package/dist/features/prepared-graphics-store.d.ts +2 -1
  112. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  113. package/dist/features/prepared-graphics.d.ts +11 -21
  114. package/dist/features/prepared-graphics.d.ts.map +1 -1
  115. package/dist/features/render-graph-compute.d.ts +2 -2
  116. package/dist/features/render-graph-compute.d.ts.map +1 -1
  117. package/dist/features/render-graph-contribution.d.ts +4 -7
  118. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  119. package/dist/features/render-graph-raster.d.ts +2 -2
  120. package/dist/features/render-graph-raster.d.ts.map +1 -1
  121. package/dist/features/render-graph-resources.d.ts +6 -6
  122. package/dist/features/render-graph-resources.d.ts.map +1 -1
  123. package/dist/features/targets.d.ts +5 -5
  124. package/dist/features/targets.d.ts.map +1 -1
  125. package/dist/features/types.d.ts +11 -190
  126. package/dist/features/types.d.ts.map +1 -1
  127. package/dist/features/vocabulary.d.ts +14 -0
  128. package/dist/features/vocabulary.d.ts.map +1 -0
  129. package/dist/fullscreen-post-process-pass.d.ts +4 -25
  130. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  131. package/dist/glyph-text-layout-system.d.ts +3 -2
  132. package/dist/glyph-text-layout-system.d.ts.map +1 -1
  133. package/dist/gpu-driven/batch-topology.d.ts +6 -75
  134. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  135. package/dist/gpu-driven/production-raster.d.ts +13 -112
  136. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  137. package/dist/gpu-driven/view-gpu.d.ts +10 -66
  138. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  139. package/dist/gpu-resource.d.ts +3 -3
  140. package/dist/gpu-scene-schema.d.ts +1 -94
  141. package/dist/gpu-scene-schema.d.ts.map +1 -1
  142. package/dist/gpu-scene.d.ts +7 -33
  143. package/dist/gpu-scene.d.ts.map +1 -1
  144. package/dist/hdrp-buffers.d.ts +3 -53
  145. package/dist/hdrp-buffers.d.ts.map +1 -1
  146. package/dist/ibl/IblPipelineCache.d.ts +74 -180
  147. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  148. package/dist/ibl/skylight-bind-group.d.ts +1 -1
  149. package/dist/index.d.ts +13 -43
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.mjs +165 -3
  152. package/dist/index.mjs.map +1 -1
  153. package/dist/inspection-types.d.ts +69 -0
  154. package/dist/inspection-types.d.ts.map +1 -0
  155. package/dist/instance-buffer-cache.d.ts +1 -4
  156. package/dist/instance-buffer-cache.d.ts.map +1 -1
  157. package/dist/internal.d.ts +1 -73
  158. package/dist/internal.d.ts.map +1 -1
  159. package/dist/internal.mjs +1 -43
  160. package/dist/internal.mjs.map +1 -1
  161. package/dist/lifecycle.d.ts +36 -2
  162. package/dist/lifecycle.d.ts.map +1 -1
  163. package/dist/materials.d.ts +0 -3
  164. package/dist/materials.d.ts.map +1 -1
  165. package/dist/mesh-material-bindings.d.ts +0 -1
  166. package/dist/mesh-material-bindings.d.ts.map +1 -1
  167. package/dist/pbr-pipeline.d.ts +75 -29
  168. package/dist/pbr-pipeline.d.ts.map +1 -1
  169. package/dist/pipeline/standard-forward-lane.d.ts +5 -0
  170. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -0
  171. package/dist/pipeline/standard-pipeline.d.ts +31 -0
  172. package/dist/pipeline/standard-pipeline.d.ts.map +1 -0
  173. package/dist/pipeline/standard-profile.d.ts +40 -0
  174. package/dist/pipeline/standard-profile.d.ts.map +1 -0
  175. package/dist/pipeline-builder.d.ts +3 -3
  176. package/dist/pipeline-builder.d.ts.map +1 -1
  177. package/dist/pipeline-spec-types.d.ts +24 -0
  178. package/dist/pipeline-spec-types.d.ts.map +1 -0
  179. package/dist/pipeline-spec.d.ts +16 -155
  180. package/dist/pipeline-spec.d.ts.map +1 -1
  181. package/dist/plugin.d.ts +1 -14
  182. package/dist/plugin.d.ts.map +1 -1
  183. package/dist/post-process-errors.d.ts +18 -18
  184. package/dist/post-process-errors.d.ts.map +1 -1
  185. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -5
  186. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  187. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts +2 -0
  188. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts.map +1 -0
  189. package/dist/record/frame-lighting.d.ts +7 -73
  190. package/dist/record/frame-lighting.d.ts.map +1 -1
  191. package/dist/record/frame-snapshot.d.ts +44 -44
  192. package/dist/record/frame-snapshot.d.ts.map +1 -1
  193. package/dist/record/frame-targets.d.ts +5 -38
  194. package/dist/record/frame-targets.d.ts.map +1 -1
  195. package/dist/record/frame.d.ts +64 -52
  196. package/dist/record/frame.d.ts.map +1 -1
  197. package/dist/record/helpers.d.ts +3 -2
  198. package/dist/record/helpers.d.ts.map +1 -1
  199. package/dist/record/main-pass-geometry.d.ts +5 -4
  200. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  201. package/dist/record/main-pass-material.d.ts +90 -59
  202. package/dist/record/main-pass-material.d.ts.map +1 -1
  203. package/dist/record/main-pass-sprite-draws.d.ts +28 -3
  204. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  205. package/dist/record/main-pass.d.ts +1 -1
  206. package/dist/record/main-pass.d.ts.map +1 -1
  207. package/dist/record/mesh-ssbo.d.ts +276 -3
  208. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  209. package/dist/record/render-context.d.ts +235 -0
  210. package/dist/record/render-context.d.ts.map +1 -0
  211. package/dist/record/shadow-pass.d.ts +15 -88
  212. package/dist/record/shadow-pass.d.ts.map +1 -1
  213. package/dist/record/skybox-post-pass.d.ts +1 -1
  214. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  215. package/dist/record/typed-frame-graph.d.ts +29 -7
  216. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  217. package/dist/record/view-ubo.d.ts +3 -3
  218. package/dist/record/view-ubo.d.ts.map +1 -1
  219. package/dist/render-contract.d.ts +416 -0
  220. package/dist/render-contract.d.ts.map +1 -0
  221. package/dist/render-data.d.ts.map +1 -1
  222. package/dist/render-graph-primitives.d.ts +14 -362
  223. package/dist/render-graph-primitives.d.ts.map +1 -1
  224. package/dist/render-pipeline.d.ts +25 -64
  225. package/dist/render-pipeline.d.ts.map +1 -1
  226. package/dist/render-system-extract.d.ts +107 -338
  227. package/dist/render-system-extract.d.ts.map +1 -1
  228. package/dist/render-system.d.ts +21 -713
  229. package/dist/render-system.d.ts.map +1 -1
  230. package/dist/scene/render-scene-types.d.ts +58 -0
  231. package/dist/scene/render-scene-types.d.ts.map +1 -0
  232. package/dist/scene/render-scene.d.ts +105 -0
  233. package/dist/scene/render-scene.d.ts.map +1 -0
  234. package/dist/scene-instances/post-spawn-resolve-joints.d.ts +2 -5
  235. package/dist/scene-instances/post-spawn-resolve-joints.d.ts.map +1 -1
  236. package/dist/ssao-buffers.d.ts +22 -3
  237. package/dist/ssao-buffers.d.ts.map +1 -1
  238. package/dist/systems/skin-palette-allocator.d.ts +2 -4
  239. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  240. package/dist/systems/skin-palette-types.d.ts +14 -0
  241. package/dist/systems/skin-palette-types.d.ts.map +1 -0
  242. package/dist/systems/transparent-sort-config.d.ts +23 -2
  243. package/dist/systems/transparent-sort-config.d.ts.map +1 -1
  244. package/dist/tilemap-chunk-extract-system.d.ts +4 -3
  245. package/dist/tilemap-chunk-extract-system.d.ts.map +1 -1
  246. package/dist/typed-render-graph-primitives.d.ts +2 -4
  247. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  248. package/dist/typed-shadow-passes.d.ts +3 -3
  249. package/dist/typed-shadow-passes.d.ts.map +1 -1
  250. package/package.json +23 -23
  251. package/src/__tests__/backend-selection-rhi-capture.test-d.ts +1 -1
  252. package/src/__tests__/callback-inference.test-d.ts +6 -3
  253. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -4
  254. package/src/__tests__/construct-renderer-identity.unit.test.ts +15 -2
  255. package/src/__tests__/device-scope-lifecycle.unit.test.ts +151 -0
  256. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +42 -0
  257. package/src/__tests__/direct-extract-consumer-contract.test-d.ts +8 -19
  258. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -13
  259. package/src/__tests__/factory-contract.integration.test.ts +158 -209
  260. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +1 -4
  261. package/src/__tests__/frame-observation.contract.test.ts +17 -8
  262. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +32 -0
  263. package/src/__tests__/glyph-text-layout-recovery.unit.test.ts +6 -11
  264. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +9 -16
  265. package/src/__tests__/gpu-driven-production.integration.test.ts +32 -205
  266. package/src/__tests__/gpu-driven-scaling.unit.test.ts +4 -7
  267. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +87 -8
  268. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +170 -8
  269. package/src/{gpu-driven/view-reference.ts → __tests__/gpu-driven-view-reference.ts} +4 -3
  270. package/src/__tests__/gpu-driven-view.browser.test.ts +12 -1
  271. package/src/__tests__/gpu-driven-view.dawn.test.ts +12 -1
  272. package/src/__tests__/gpu-driven-view.unit.test.ts +11 -267
  273. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +3 -3
  274. package/src/__tests__/gpu-scene-render-graph-gpu.ts +2 -3
  275. package/src/__tests__/gpu-scene.dawn.test.ts +2 -6
  276. package/src/__tests__/gpu-scene.unit.test.ts +10 -24
  277. package/src/__tests__/ibl/face-uniforms.unit.test.ts +46 -0
  278. package/src/__tests__/infinite-grid-no-vertex-probe.unit.test.ts +34 -66
  279. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -5
  280. package/src/__tests__/light-buffer-layout.unit.test.ts +4 -49
  281. package/src/__tests__/material-cooked-projection.unit.test.ts +2 -51
  282. package/src/__tests__/material-snapshot-mutation.integration.test.ts +10 -15
  283. package/src/__tests__/material-static-dynamic-route.unit.test.ts +4 -23
  284. package/src/__tests__/material-stride-surface.unit.test.ts +2 -2
  285. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +7 -8
  286. package/src/__tests__/morph-culling-reentry.dawn.test.ts +29 -38
  287. package/src/__tests__/morph-feature-gpu-work.integration.test.ts +45 -43
  288. package/src/__tests__/morph-feature-plan.unit.test.ts +53 -0
  289. package/src/__tests__/morph-visual-evidence.integration.test.ts +47 -33
  290. package/src/__tests__/nineslice-handle-surface.unit.test.ts +11 -6
  291. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -13
  292. package/src/__tests__/prepared-graphics-capability.unit.test.ts +6 -81
  293. package/src/__tests__/prepared-graphics-generation.unit.test.ts +3 -17
  294. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +6 -74
  295. package/src/__tests__/prepared-graphics-resolver.unit.test.ts +1 -3
  296. package/src/__tests__/prepared-graphics-snapshot.unit.test.ts +2 -2
  297. package/src/__tests__/profiler-default-off.test.ts +1 -1
  298. package/src/__tests__/profiler-phase-catalog.test.ts +2 -2
  299. package/src/__tests__/profiler-phase-integration.test.ts +5 -13
  300. package/src/__tests__/profiler-phase-token.test.ts +3 -8
  301. package/src/__tests__/public-surface.test-d.ts +65 -3
  302. package/src/__tests__/recover-error-policy-owner.unit.test.ts +1 -21
  303. package/src/__tests__/render-error-code-owner.test-d.ts +18 -23
  304. package/src/__tests__/render-error-detail-ssot.test-d.ts +0 -95
  305. package/src/__tests__/render-error-exhaustive.test-d.ts +8 -1
  306. package/src/__tests__/render-feature-assembly.test-d.ts +8 -16
  307. package/src/__tests__/render-feature-context.test-d.ts +16 -40
  308. package/src/__tests__/render-feature-contract.test-d.ts +9 -18
  309. package/src/__tests__/render-feature-diagnostics.unit.test.ts +6 -8
  310. package/src/__tests__/render-feature-docs.unit.test.ts +8 -7
  311. package/src/__tests__/render-feature-gpu-work.integration.test.ts +14 -75
  312. package/src/__tests__/render-feature-plan.test-d.ts +161 -0
  313. package/src/__tests__/render-feature-plan.unit.test.ts +170 -0
  314. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +8 -19
  315. package/src/__tests__/render-feature-prepared-errors.test-d.ts +3 -6
  316. package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +10 -71
  317. package/src/__tests__/render-feature-prepared-graphics-isolation.unit.test.ts +11 -61
  318. package/src/__tests__/render-feature-prepared-graphics-negative.test-d.ts +20 -43
  319. package/src/__tests__/render-feature-prepared-graphics.test-d.ts +56 -78
  320. package/src/__tests__/render-feature-public-surface.test-d.ts +6 -24
  321. package/src/__tests__/render-feature-retry.unit.test.ts +7 -48
  322. package/src/__tests__/render-feature-scheduling.unit.test.ts +15 -27
  323. package/src/__tests__/render-feature-stage-failed-policy-owner.unit.test.ts +0 -9
  324. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +19 -109
  325. package/src/__tests__/render-feature-visibility-context.test-d.ts +1 -6
  326. package/src/__tests__/render-feature-visibility-context.unit.test.ts +1 -2
  327. package/src/__tests__/render-feature-zero-work.unit.test.ts +14 -20
  328. package/src/__tests__/render-scene-projection.unit.test.ts +73 -111
  329. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +46 -50
  330. package/src/__tests__/renderer-lifecycle.integration.test.ts +49 -1
  331. package/src/__tests__/shader-stage-surface.unit.test.ts +14 -9
  332. package/src/__tests__/ssao-config.unit.test.ts +36 -0
  333. package/src/__tests__/ssao-kernel-surface.unit.test.ts +4 -5
  334. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +28 -0
  335. package/src/__tests__/standard-descriptors.test-d.ts +22 -0
  336. package/src/__tests__/standard-pipeline.dawn.test.ts +61 -0
  337. package/src/__tests__/standard-pipeline.integration.test.ts +124 -0
  338. package/src/__tests__/standard-profile-oracle.unit.test.ts +62 -0
  339. package/src/__tests__/surface-configuration.unit.test.ts +1 -2
  340. package/src/__tests__/texture-usage-surface.unit.test.ts +3 -6
  341. package/src/__tests__/typed-pipeline-topology.unit.test.ts +40 -206
  342. package/src/__tests__/urp-pipeline-targets.unit.test.ts +1 -11
  343. package/src/__tests__/urp-pipeline.unit.test.ts +2 -2
  344. package/src/__tests__/vertex-color-pipeline.unit.test.ts +10 -2
  345. package/src/__tests__/vertex-layout-consumer.unit.test.ts +1 -1
  346. package/src/__tests__/visibility-diagnostics.integration.test.ts +7 -11
  347. package/src/__tests__/visibility-extract.integration.test.ts +20 -11
  348. package/src/__tests__/visibility-extract.perf.test.ts +66 -32
  349. package/src/__tests__/visibility-feature-dedup.integration.test.ts +2 -4
  350. package/src/__tests__/visibility-hidden-report.unit.test.ts +1 -2
  351. package/src/__tests__/visibility-instances.integration.test.ts +7 -11
  352. package/src/__tests__/visibility-multi-world.integration.test.ts +38 -6
  353. package/src/__tests__/visibility-producer-matrix.integration.test.ts +18 -41
  354. package/src/__tests__/visibility-public-surface.test-d.ts +10 -9
  355. package/src/__tests__/visibility-resolver.unit.test.ts +2 -1
  356. package/src/__tests__/visibility-resource-short-circuit.unit.test.ts +80 -0
  357. package/src/__tests__/visibility-snapshot-contract.unit.test.ts +2 -1
  358. package/src/__tests__/vocabulary.unit.test.ts +1 -1
  359. package/src/assembly/backend-contract.ts +57 -0
  360. package/src/{renderer/renderer-factory.ts → assembly/factory.ts} +1709 -2693
  361. package/src/assembly/host-contract.ts +101 -0
  362. package/src/{renderer → assembly}/material/assembly.ts +1 -3
  363. package/src/assembly/material/pipeline-projection.ts +63 -0
  364. package/src/authoring.ts +14 -22
  365. package/src/components/__tests__/schema-contract.unit.test.ts +10 -0
  366. package/src/components/glyph-text.ts +3 -2
  367. package/src/components/index.ts +7 -9
  368. package/src/components/instances.ts +4 -5
  369. package/src/components/light-helpers.ts +1 -1
  370. package/src/components/mesh-filter.ts +11 -10
  371. package/src/components/post-process-params.ts +3 -3
  372. package/src/components/scene-instance.ts +12 -12
  373. package/src/components/skybox-background.ts +103 -0
  374. package/src/components/{environment.ts → skylight.ts} +2 -111
  375. package/src/components/sprite-animation.ts +3 -4
  376. package/src/components/sprite-instances.ts +2 -2
  377. package/src/components/sprite-playback-mode.ts +7 -1
  378. package/src/components/sprite-region-override.ts +2 -3
  379. package/src/components/tile-layer.ts +8 -2
  380. package/src/construct-renderer.ts +67 -4
  381. package/src/debug-draw-glue.ts +8 -129
  382. package/src/device/device-scope.ts +306 -0
  383. package/src/{gpu-resource-store.ts → device/gpu-residency.ts} +540 -639
  384. package/src/device/resource-types.ts +20 -0
  385. package/src/engine-metrics.ts +12 -19
  386. package/src/errors/recover.ts +45 -3
  387. package/src/errors/render.ts +357 -460
  388. package/src/features/fullscreen.ts +46 -0
  389. package/src/features/host.ts +575 -309
  390. package/src/features/morph/morph-feature.ts +195 -105
  391. package/src/features/plan.ts +447 -0
  392. package/src/features/prepared-gpu-work.ts +72 -15
  393. package/src/features/prepared-graphics-store.ts +20 -6
  394. package/src/features/prepared-graphics.ts +12 -71
  395. package/src/features/render-graph-compute.ts +14 -5
  396. package/src/features/render-graph-contribution.ts +43 -63
  397. package/src/features/render-graph-raster.ts +11 -6
  398. package/src/features/render-graph-resources.ts +25 -23
  399. package/src/features/targets.ts +8 -21
  400. package/src/features/types.ts +39 -227
  401. package/src/features/vocabulary.ts +24 -0
  402. package/src/fullscreen-post-process-pass.ts +14 -57
  403. package/src/glyph-text-layout-system.ts +70 -96
  404. package/src/gpu-driven/batch-topology.ts +24 -202
  405. package/src/gpu-driven/production-raster.ts +93 -1108
  406. package/src/gpu-driven/view-gpu.ts +46 -192
  407. package/src/gpu-resource.ts +4 -4
  408. package/src/gpu-scene-schema.ts +11 -99
  409. package/src/gpu-scene.ts +231 -364
  410. package/src/hdrp-buffers.ts +12 -221
  411. package/src/ibl/IblPipelineCache.ts +471 -711
  412. package/src/ibl/skylight-bind-group.ts +4 -4
  413. package/src/index.ts +58 -217
  414. package/src/inspection-types.ts +76 -0
  415. package/src/instance-buffer-cache.ts +1 -29
  416. package/src/internal.ts +6 -85
  417. package/src/lifecycle.ts +64 -19
  418. package/src/materials.ts +7 -29
  419. package/src/mesh-material-bindings.ts +0 -1
  420. package/src/pbr-pipeline.ts +434 -53
  421. package/src/{urp-pipeline.ts → pipeline/standard-forward-lane.ts} +38 -149
  422. package/src/{hdrp-pipeline.ts → pipeline/standard-pipeline.ts} +221 -191
  423. package/src/pipeline/standard-profile.ts +65 -0
  424. package/src/pipeline-builder.ts +60 -64
  425. package/src/pipeline-spec-types.ts +28 -0
  426. package/src/pipeline-spec.ts +72 -609
  427. package/src/plugin.ts +3 -75
  428. package/src/post-process-errors.ts +31 -33
  429. package/src/prepare/prepared-graphics-resolver.ts +5 -4
  430. package/src/record/__tests__/material-slot-plan.unit.test.ts +1 -33
  431. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +41 -0
  432. package/src/record/__tests__/video-texture-view.unit.test.ts +1 -1
  433. package/src/record/frame-lighting.ts +74 -195
  434. package/src/record/frame-snapshot.ts +45 -60
  435. package/src/record/frame-targets.ts +8 -243
  436. package/src/record/frame.ts +509 -796
  437. package/src/record/helpers.ts +6 -11
  438. package/src/record/main-pass-geometry.ts +106 -147
  439. package/src/record/main-pass-material.ts +517 -190
  440. package/src/record/main-pass-sprite-draws.ts +198 -117
  441. package/src/record/main-pass.ts +162 -143
  442. package/src/record/mesh-ssbo.ts +517 -38
  443. package/src/record/render-context.ts +364 -0
  444. package/src/record/shadow-pass.ts +138 -1838
  445. package/src/record/skybox-post-pass.ts +12 -14
  446. package/src/record/typed-frame-graph.ts +215 -254
  447. package/src/record/view-ubo.ts +6 -37
  448. package/src/render-contract.ts +571 -0
  449. package/src/render-data.ts +3 -3
  450. package/src/render-graph-primitives.ts +76 -851
  451. package/src/render-pipeline.ts +104 -188
  452. package/src/render-system-extract.ts +1048 -1729
  453. package/src/render-system.ts +330 -1957
  454. package/src/scene/render-scene-types.ts +64 -0
  455. package/src/scene/render-scene.ts +1032 -0
  456. package/src/scene-instances/post-spawn-resolve-joints.ts +4 -7
  457. package/src/ssao-buffers.ts +167 -8
  458. package/src/systems/skin-palette-allocator.ts +21 -64
  459. package/src/systems/skin-palette-types.ts +14 -0
  460. package/src/systems/transparent-sort-config.ts +25 -3
  461. package/src/tilemap-chunk-extract-system.ts +53 -70
  462. package/src/typed-render-graph-primitives.ts +35 -24
  463. package/src/typed-shadow-passes.ts +26 -56
  464. package/dist/__tests__/analytic-sky-contract.test.d.ts +0 -2
  465. package/dist/__tests__/analytic-sky-contract.test.d.ts.map +0 -1
  466. package/dist/__tests__/analytic-sky-falsify.test.d.ts +0 -2
  467. package/dist/__tests__/analytic-sky-falsify.test.d.ts.map +0 -1
  468. package/dist/__tests__/assets-runtime-handoff.integration.test.d.ts +0 -2
  469. package/dist/__tests__/assets-runtime-handoff.integration.test.d.ts.map +0 -1
  470. package/dist/__tests__/backend-selection.integration.test.d.ts +0 -2
  471. package/dist/__tests__/backend-selection.integration.test.d.ts.map +0 -1
  472. package/dist/__tests__/camera-temporal.test-d.d.ts +0 -2
  473. package/dist/__tests__/camera-temporal.test-d.d.ts.map +0 -1
  474. package/dist/__tests__/camera-temporal.test.d.ts +0 -2
  475. package/dist/__tests__/camera-temporal.test.d.ts.map +0 -1
  476. package/dist/__tests__/directional-shadow-probe-atlas-layout.unit.test.d.ts +0 -2
  477. package/dist/__tests__/directional-shadow-probe-atlas-layout.unit.test.d.ts.map +0 -1
  478. package/dist/__tests__/environment-inspection.test.d.ts +0 -2
  479. package/dist/__tests__/environment-inspection.test.d.ts.map +0 -1
  480. package/dist/__tests__/fog-ai-user.fixture.d.ts +0 -2
  481. package/dist/__tests__/fog-ai-user.fixture.d.ts.map +0 -1
  482. package/dist/__tests__/fog-custom-render-feature.fixture.d.ts +0 -16
  483. package/dist/__tests__/fog-custom-render-feature.fixture.d.ts.map +0 -1
  484. package/dist/__tests__/fog-custom-render-feature.integration.test.d.ts +0 -2
  485. package/dist/__tests__/fog-custom-render-feature.integration.test.d.ts.map +0 -1
  486. package/dist/__tests__/fog-inspection.integration.test.d.ts +0 -2
  487. package/dist/__tests__/fog-inspection.integration.test.d.ts.map +0 -1
  488. package/dist/__tests__/fog-owner-state.integration.test.d.ts +0 -2
  489. package/dist/__tests__/fog-owner-state.integration.test.d.ts.map +0 -1
  490. package/dist/__tests__/fog-pipeline-topology.integration.test.d.ts +0 -2
  491. package/dist/__tests__/fog-pipeline-topology.integration.test.d.ts.map +0 -1
  492. package/dist/__tests__/fog-public-surface.integration.test.d.ts +0 -2
  493. package/dist/__tests__/fog-public-surface.integration.test.d.ts.map +0 -1
  494. package/dist/__tests__/fog-ray-contract.integration.test.d.ts +0 -2
  495. package/dist/__tests__/fog-ray-contract.integration.test.d.ts.map +0 -1
  496. package/dist/__tests__/fog-source-absence.integration.test.d.ts +0 -2
  497. package/dist/__tests__/fog-source-absence.integration.test.d.ts.map +0 -1
  498. package/dist/__tests__/fog-temporal.integration.test.d.ts +0 -2
  499. package/dist/__tests__/fog-temporal.integration.test.d.ts.map +0 -1
  500. package/dist/__tests__/frame-chain.unit.test.d.ts +0 -2
  501. package/dist/__tests__/frame-chain.unit.test.d.ts.map +0 -1
  502. package/dist/__tests__/gpu-scene-field-type-owner.test-d.d.ts +0 -2
  503. package/dist/__tests__/gpu-scene-field-type-owner.test-d.d.ts.map +0 -1
  504. package/dist/__tests__/hdrp-pipeline.unit.test.d.ts +0 -2
  505. package/dist/__tests__/hdrp-pipeline.unit.test.d.ts.map +0 -1
  506. package/dist/__tests__/ibl/ibl-fallback.unit.test.d.ts +0 -2
  507. package/dist/__tests__/ibl/ibl-fallback.unit.test.d.ts.map +0 -1
  508. package/dist/__tests__/material-derived-error-stop-draw.integration.test.d.ts +0 -2
  509. package/dist/__tests__/material-derived-error-stop-draw.integration.test.d.ts.map +0 -1
  510. package/dist/__tests__/material-derived-payload.integration.test.d.ts +0 -2
  511. package/dist/__tests__/material-derived-payload.integration.test.d.ts.map +0 -1
  512. package/dist/__tests__/materials.unit.test.d.ts +0 -2
  513. package/dist/__tests__/materials.unit.test.d.ts.map +0 -1
  514. package/dist/__tests__/param-schema-projection-census.unit.test.d.ts +0 -2
  515. package/dist/__tests__/param-schema-projection-census.unit.test.d.ts.map +0 -1
  516. package/dist/__tests__/persistent-render-scene.unit.test.d.ts +0 -2
  517. package/dist/__tests__/persistent-render-scene.unit.test.d.ts.map +0 -1
  518. package/dist/__tests__/pipeline-error-policy-owner.unit.test.d.ts +0 -2
  519. package/dist/__tests__/pipeline-error-policy-owner.unit.test.d.ts.map +0 -1
  520. package/dist/__tests__/pipeline-pass-trace.test.d.ts +0 -2
  521. package/dist/__tests__/pipeline-pass-trace.test.d.ts.map +0 -1
  522. package/dist/__tests__/pipeline-stage-owner.test-d.d.ts +0 -2
  523. package/dist/__tests__/pipeline-stage-owner.test-d.d.ts.map +0 -1
  524. package/dist/__tests__/plugin.test.d.ts +0 -2
  525. package/dist/__tests__/plugin.test.d.ts.map +0 -1
  526. package/dist/__tests__/prepared-graphics-backend-matrix.unit.test.d.ts +0 -2
  527. package/dist/__tests__/prepared-graphics-backend-matrix.unit.test.d.ts.map +0 -1
  528. package/dist/__tests__/prepared-graphics-resolver-recording.unit.test.d.ts +0 -2
  529. package/dist/__tests__/prepared-graphics-resolver-recording.unit.test.d.ts.map +0 -1
  530. package/dist/__tests__/public-pipeline-id.test-d.d.ts +0 -2
  531. package/dist/__tests__/public-pipeline-id.test-d.d.ts.map +0 -1
  532. package/dist/__tests__/render-dispatch-membership.unit.test.d.ts +0 -2
  533. package/dist/__tests__/render-dispatch-membership.unit.test.d.ts.map +0 -1
  534. package/dist/__tests__/render-feature-construction.unit.test.d.ts +0 -2
  535. package/dist/__tests__/render-feature-construction.unit.test.d.ts.map +0 -1
  536. package/dist/__tests__/render-feature-error-code-owner.test-d.d.ts +0 -2
  537. package/dist/__tests__/render-feature-error-code-owner.test-d.d.ts.map +0 -1
  538. package/dist/__tests__/render-feature-gpu-buffer-usage-owner.test-d.d.ts +0 -2
  539. package/dist/__tests__/render-feature-gpu-buffer-usage-owner.test-d.d.ts.map +0 -1
  540. package/dist/__tests__/render-feature-graph.integration.test.d.ts +0 -2
  541. package/dist/__tests__/render-feature-graph.integration.test.d.ts.map +0 -1
  542. package/dist/__tests__/render-feature-graphics-staging.unit.test.d.ts +0 -2
  543. package/dist/__tests__/render-feature-graphics-staging.unit.test.d.ts.map +0 -1
  544. package/dist/__tests__/render-feature-order.unit.test.d.ts +0 -2
  545. package/dist/__tests__/render-feature-order.unit.test.d.ts.map +0 -1
  546. package/dist/__tests__/render-feature-prepared-graphics-recording.unit.test.d.ts +0 -2
  547. package/dist/__tests__/render-feature-prepared-graphics-recording.unit.test.d.ts.map +0 -1
  548. package/dist/__tests__/render-feature-staging.unit.test.d.ts +0 -2
  549. package/dist/__tests__/render-feature-staging.unit.test.d.ts.map +0 -1
  550. package/dist/__tests__/renderer-render-scene-public.test-d.d.ts +0 -2
  551. package/dist/__tests__/renderer-render-scene-public.test-d.d.ts.map +0 -1
  552. package/dist/__tests__/scene-temporal-coverage.unit.test.d.ts +0 -2
  553. package/dist/__tests__/scene-temporal-coverage.unit.test.d.ts.map +0 -1
  554. package/dist/__tests__/sun-snapshot-consumers.test.d.ts +0 -2
  555. package/dist/__tests__/sun-snapshot-consumers.test.d.ts.map +0 -1
  556. package/dist/__tests__/surface-backend-kind-owner.test-d.d.ts +0 -2
  557. package/dist/__tests__/surface-backend-kind-owner.test-d.d.ts.map +0 -1
  558. package/dist/__tests__/taa-history-store.unit.test.d.ts +0 -2
  559. package/dist/__tests__/taa-history-store.unit.test.d.ts.map +0 -1
  560. package/dist/__tests__/temporal-frame-transaction.integration.test.d.ts +0 -2
  561. package/dist/__tests__/temporal-frame-transaction.integration.test.d.ts.map +0 -1
  562. package/dist/__tests__/temporal-inspection.unit.test.d.ts +0 -2
  563. package/dist/__tests__/temporal-inspection.unit.test.d.ts.map +0 -1
  564. package/dist/assets/__tests__/asset-decoders.unit.test.d.ts +0 -2
  565. package/dist/assets/__tests__/asset-decoders.unit.test.d.ts.map +0 -1
  566. package/dist/assets/__tests__/mipmap-generator.unit.test.d.ts +0 -2
  567. package/dist/assets/__tests__/mipmap-generator.unit.test.d.ts.map +0 -1
  568. package/dist/assets/__tests__/render-asset-port.unit.test.d.ts +0 -2
  569. package/dist/assets/__tests__/render-asset-port.unit.test.d.ts.map +0 -1
  570. package/dist/assets/__tests__/resolve-render-asset.unit.test.d.ts +0 -2
  571. package/dist/assets/__tests__/resolve-render-asset.unit.test.d.ts.map +0 -1
  572. package/dist/chunk-33UF5SAO.mjs +0 -7397
  573. package/dist/chunk-33UF5SAO.mjs.map +0 -1
  574. package/dist/chunk-A5QCJSNL.mjs +0 -29913
  575. package/dist/chunk-A5QCJSNL.mjs.map +0 -1
  576. package/dist/chunk-JMSB7AR4.mjs +0 -3
  577. package/dist/chunk-JMSB7AR4.mjs.map +0 -1
  578. package/dist/chunk-LXNGO6AO.mjs +0 -569
  579. package/dist/chunk-LXNGO6AO.mjs.map +0 -1
  580. package/dist/chunk-QRXPSX4X.mjs +0 -659
  581. package/dist/chunk-QRXPSX4X.mjs.map +0 -1
  582. package/dist/chunk-YAKEQPWO.mjs +0 -1439
  583. package/dist/chunk-YAKEQPWO.mjs.map +0 -1
  584. package/dist/components/__tests__/atmosphere-red.test.d.ts +0 -2
  585. package/dist/components/__tests__/atmosphere-red.test.d.ts.map +0 -1
  586. package/dist/components/__tests__/atmosphere.test.d.ts +0 -2
  587. package/dist/components/__tests__/atmosphere.test.d.ts.map +0 -1
  588. package/dist/components/__tests__/fog.test-d.d.ts +0 -2
  589. package/dist/components/__tests__/fog.test-d.d.ts.map +0 -1
  590. package/dist/components/__tests__/fog.test.d.ts +0 -2
  591. package/dist/components/__tests__/fog.test.d.ts.map +0 -1
  592. package/dist/components/atmosphere.d.ts +0 -18
  593. package/dist/components/atmosphere.d.ts.map +0 -1
  594. package/dist/components/environment.d.ts +0 -116
  595. package/dist/components/environment.d.ts.map +0 -1
  596. package/dist/deferred-lighting.d.ts +0 -13
  597. package/dist/deferred-lighting.d.ts.map +0 -1
  598. package/dist/dynamic-texture-store.d.ts +0 -42
  599. package/dist/dynamic-texture-store.d.ts.map +0 -1
  600. package/dist/environment/__tests__/failure-injection.test.d.ts +0 -2
  601. package/dist/environment/__tests__/failure-injection.test.d.ts.map +0 -1
  602. package/dist/environment/__tests__/generation-red.test.d.ts +0 -2
  603. package/dist/environment/__tests__/generation-red.test.d.ts.map +0 -1
  604. package/dist/environment/__tests__/generation.test.d.ts +0 -2
  605. package/dist/environment/__tests__/generation.test.d.ts.map +0 -1
  606. package/dist/environment/__tests__/graph-transaction.test.d.ts +0 -2
  607. package/dist/environment/__tests__/graph-transaction.test.d.ts.map +0 -1
  608. package/dist/environment/__tests__/inspection-red.test.d.ts +0 -2
  609. package/dist/environment/__tests__/inspection-red.test.d.ts.map +0 -1
  610. package/dist/environment/__tests__/lifecycle-evidence.test.d.ts +0 -2
  611. package/dist/environment/__tests__/lifecycle-evidence.test.d.ts.map +0 -1
  612. package/dist/environment/__tests__/pipeline-topology.test.d.ts +0 -2
  613. package/dist/environment/__tests__/pipeline-topology.test.d.ts.map +0 -1
  614. package/dist/environment/__tests__/readiness.test.d.ts +0 -2
  615. package/dist/environment/__tests__/readiness.test.d.ts.map +0 -1
  616. package/dist/environment/__tests__/recovery.test.d.ts +0 -2
  617. package/dist/environment/__tests__/recovery.test.d.ts.map +0 -1
  618. package/dist/environment/__tests__/resize-dispose.test.d.ts +0 -2
  619. package/dist/environment/__tests__/resize-dispose.test.d.ts.map +0 -1
  620. package/dist/environment/__tests__/signature-red.test.d.ts +0 -2
  621. package/dist/environment/__tests__/signature-red.test.d.ts.map +0 -1
  622. package/dist/environment/__tests__/signature.test.d.ts +0 -2
  623. package/dist/environment/__tests__/signature.test.d.ts.map +0 -1
  624. package/dist/environment/descriptors.d.ts +0 -2
  625. package/dist/environment/descriptors.d.ts.map +0 -1
  626. package/dist/environment/extracted-environment.d.ts +0 -53
  627. package/dist/environment/extracted-environment.d.ts.map +0 -1
  628. package/dist/environment/generation.d.ts +0 -62
  629. package/dist/environment/generation.d.ts.map +0 -1
  630. package/dist/environment/gpu-bundle.d.ts +0 -23
  631. package/dist/environment/gpu-bundle.d.ts.map +0 -1
  632. package/dist/environment/graph.d.ts +0 -108
  633. package/dist/environment/graph.d.ts.map +0 -1
  634. package/dist/environment/inspection.d.ts +0 -48
  635. package/dist/environment/inspection.d.ts.map +0 -1
  636. package/dist/environment/lifecycle.d.ts +0 -22
  637. package/dist/environment/lifecycle.d.ts.map +0 -1
  638. package/dist/environment/readiness.d.ts +0 -29
  639. package/dist/environment/readiness.d.ts.map +0 -1
  640. package/dist/environment/signature.d.ts +0 -70
  641. package/dist/environment/signature.d.ts.map +0 -1
  642. package/dist/extract/__tests__/directional-csm-domain.unit.test.d.ts +0 -2
  643. package/dist/extract/__tests__/directional-csm-domain.unit.test.d.ts.map +0 -1
  644. package/dist/extract/__tests__/environment-source.test.d.ts +0 -2
  645. package/dist/extract/__tests__/environment-source.test.d.ts.map +0 -1
  646. package/dist/extract/__tests__/schema-readers.unit.test.d.ts +0 -2
  647. package/dist/extract/__tests__/schema-readers.unit.test.d.ts.map +0 -1
  648. package/dist/extract/directional-csm.d.ts +0 -129
  649. package/dist/extract/directional-csm.d.ts.map +0 -1
  650. package/dist/extract/index.d.ts +0 -5
  651. package/dist/extract/index.d.ts.map +0 -1
  652. package/dist/extract/material-interface.d.ts +0 -56
  653. package/dist/extract/material-interface.d.ts.map +0 -1
  654. package/dist/extract/schema-readers.d.ts +0 -20
  655. package/dist/extract/schema-readers.d.ts.map +0 -1
  656. package/dist/features/graph-contribution.d.ts +0 -64
  657. package/dist/features/graph-contribution.d.ts.map +0 -1
  658. package/dist/features/index.d.ts +0 -21
  659. package/dist/features/index.d.ts.map +0 -1
  660. package/dist/features/morph/index.d.ts +0 -3
  661. package/dist/features/morph/index.d.ts.map +0 -1
  662. package/dist/features/morph/morph-bounds.d.ts +0 -42
  663. package/dist/features/morph/morph-bounds.d.ts.map +0 -1
  664. package/dist/features/scene-temporal.d.ts +0 -45
  665. package/dist/features/scene-temporal.d.ts.map +0 -1
  666. package/dist/features/taa-resolve.d.ts +0 -21
  667. package/dist/features/taa-resolve.d.ts.map +0 -1
  668. package/dist/frame-chain.d.ts +0 -14
  669. package/dist/frame-chain.d.ts.map +0 -1
  670. package/dist/gpu-driven/view-reference.d.ts.map +0 -1
  671. package/dist/gpu-resource-store.d.ts.map +0 -1
  672. package/dist/hdrp-cluster-membership.d.ts +0 -2
  673. package/dist/hdrp-cluster-membership.d.ts.map +0 -1
  674. package/dist/hdrp-pipeline.d.ts +0 -36
  675. package/dist/hdrp-pipeline.d.ts.map +0 -1
  676. package/dist/ibl/cubemap-projection.d.ts +0 -23
  677. package/dist/ibl/cubemap-projection.d.ts.map +0 -1
  678. package/dist/ibl/face-uniforms.d.ts +0 -65
  679. package/dist/ibl/face-uniforms.d.ts.map +0 -1
  680. package/dist/mesh-update-errors.d.ts +0 -71
  681. package/dist/mesh-update-errors.d.ts.map +0 -1
  682. package/dist/mipmap-generator.d.ts +0 -39
  683. package/dist/mipmap-generator.d.ts.map +0 -1
  684. package/dist/param-schema-projection-census.d.ts +0 -18
  685. package/dist/param-schema-projection-census.d.ts.map +0 -1
  686. package/dist/persistent-render-scene.d.ts +0 -158
  687. package/dist/persistent-render-scene.d.ts.map +0 -1
  688. package/dist/pipeline/index.d.ts +0 -11
  689. package/dist/pipeline/index.d.ts.map +0 -1
  690. package/dist/pipeline-errors.d.ts +0 -110
  691. package/dist/pipeline-errors.d.ts.map +0 -1
  692. package/dist/prepare/__tests__/schema-readers.unit.test.d.ts +0 -2
  693. package/dist/prepare/__tests__/schema-readers.unit.test.d.ts.map +0 -1
  694. package/dist/prepare/index.d.ts +0 -4
  695. package/dist/prepare/index.d.ts.map +0 -1
  696. package/dist/prepare/schema-readers.d.ts +0 -11
  697. package/dist/prepare/schema-readers.d.ts.map +0 -1
  698. package/dist/record/__tests__/environment-record.test.d.ts +0 -2
  699. package/dist/record/__tests__/environment-record.test.d.ts.map +0 -1
  700. package/dist/record/__tests__/frame-targets.unit.test.d.ts +0 -2
  701. package/dist/record/__tests__/frame-targets.unit.test.d.ts.map +0 -1
  702. package/dist/record/__tests__/membership-timing.unit.test.d.ts +0 -2
  703. package/dist/record/__tests__/membership-timing.unit.test.d.ts.map +0 -1
  704. package/dist/record/__tests__/render-submission.integration.test.d.ts +0 -2
  705. package/dist/record/__tests__/render-submission.integration.test.d.ts.map +0 -1
  706. package/dist/record/__tests__/render-submission.perf.test.d.ts +0 -2
  707. package/dist/record/__tests__/render-submission.perf.test.d.ts.map +0 -1
  708. package/dist/record/__tests__/shadow-submit-mode.unit.test.d.ts +0 -2
  709. package/dist/record/__tests__/shadow-submit-mode.unit.test.d.ts.map +0 -1
  710. package/dist/record/__tests__/view-ubo-layout.unit.test.d.ts +0 -2
  711. package/dist/record/__tests__/view-ubo-layout.unit.test.d.ts.map +0 -1
  712. package/dist/record/frame-observation.d.ts +0 -38
  713. package/dist/record/frame-observation.d.ts.map +0 -1
  714. package/dist/record/index.d.ts +0 -15
  715. package/dist/record/index.d.ts.map +0 -1
  716. package/dist/record/main-pass-skin.d.ts +0 -46
  717. package/dist/record/main-pass-skin.d.ts.map +0 -1
  718. package/dist/record/main-pass-sprite.d.ts +0 -69
  719. package/dist/record/main-pass-sprite.d.ts.map +0 -1
  720. package/dist/record/material-payload.d.ts +0 -43
  721. package/dist/record/material-payload.d.ts.map +0 -1
  722. package/dist/record/material-slot-plan.d.ts +0 -26
  723. package/dist/record/material-slot-plan.d.ts.map +0 -1
  724. package/dist/record/membership-timing.d.ts +0 -232
  725. package/dist/record/membership-timing.d.ts.map +0 -1
  726. package/dist/record/shadow-caster-uniform.d.ts +0 -8
  727. package/dist/record/shadow-caster-uniform.d.ts.map +0 -1
  728. package/dist/record/shadow-submit-mode.d.ts +0 -66
  729. package/dist/record/shadow-submit-mode.d.ts.map +0 -1
  730. package/dist/render-dispatch-membership.d.ts +0 -47
  731. package/dist/render-dispatch-membership.d.ts.map +0 -1
  732. package/dist/render-pipeline-context.d.ts +0 -289
  733. package/dist/render-pipeline-context.d.ts.map +0 -1
  734. package/dist/render-pipeline-target.d.ts +0 -16
  735. package/dist/render-pipeline-target.d.ts.map +0 -1
  736. package/dist/render-scene-projection.d.ts +0 -174
  737. package/dist/render-scene-projection.d.ts.map +0 -1
  738. package/dist/render-system-fold.d.ts +0 -266
  739. package/dist/render-system-fold.d.ts.map +0 -1
  740. package/dist/renderer/asset-service.d.ts +0 -10
  741. package/dist/renderer/asset-service.d.ts.map +0 -1
  742. package/dist/renderer/backend-selection.d.ts.map +0 -1
  743. package/dist/renderer/environment-classify.d.ts +0 -6
  744. package/dist/renderer/environment-classify.d.ts.map +0 -1
  745. package/dist/renderer/environment-error.d.ts +0 -13
  746. package/dist/renderer/environment-error.d.ts.map +0 -1
  747. package/dist/renderer/material/pipeline-projection.d.ts +0 -91
  748. package/dist/renderer/material/pipeline-projection.d.ts.map +0 -1
  749. package/dist/renderer/renderer-factory.d.ts +0 -283
  750. package/dist/renderer/renderer-factory.d.ts.map +0 -1
  751. package/dist/renderer.d.ts +0 -771
  752. package/dist/renderer.d.ts.map +0 -1
  753. package/dist/scene-utils/collect-subtree.d.ts +0 -11
  754. package/dist/scene-utils/collect-subtree.d.ts.map +0 -1
  755. package/dist/ssao-data.d.ts +0 -34
  756. package/dist/ssao-data.d.ts.map +0 -1
  757. package/dist/surface-format.d.ts +0 -10
  758. package/dist/surface-format.d.ts.map +0 -1
  759. package/dist/taa-history-store.d.ts +0 -88
  760. package/dist/taa-history-store.d.ts.map +0 -1
  761. package/dist/temporal-coverage.d.ts +0 -35
  762. package/dist/temporal-coverage.d.ts.map +0 -1
  763. package/dist/temporal-frame-transaction.d.ts +0 -92
  764. package/dist/temporal-frame-transaction.d.ts.map +0 -1
  765. package/dist/temporal-inspection.d.ts +0 -57
  766. package/dist/temporal-inspection.d.ts.map +0 -1
  767. package/dist/temporal-view.d.ts +0 -43
  768. package/dist/temporal-view.d.ts.map +0 -1
  769. package/dist/tilemap/resolve-tileset-runtime.d.ts +0 -23
  770. package/dist/tilemap/resolve-tileset-runtime.d.ts.map +0 -1
  771. package/dist/urp-pipeline.d.ts +0 -4
  772. package/dist/urp-pipeline.d.ts.map +0 -1
  773. package/src/__tests__/analytic-sky-contract.test.ts +0 -27
  774. package/src/__tests__/analytic-sky-falsify-carriers.mjs +0 -43
  775. package/src/__tests__/analytic-sky-falsify.test.ts +0 -94
  776. package/src/__tests__/assets-runtime-handoff.integration.test.ts +0 -36
  777. package/src/__tests__/backend-selection.integration.test.ts +0 -11
  778. package/src/__tests__/camera-temporal.test-d.ts +0 -8
  779. package/src/__tests__/camera-temporal.test.ts +0 -188
  780. package/src/__tests__/directional-shadow-probe-atlas-layout.unit.test.ts +0 -18
  781. package/src/__tests__/environment-inspection.test.ts +0 -45
  782. package/src/__tests__/fog-ai-user.fixture.ts +0 -350
  783. package/src/__tests__/fog-custom-render-feature.fixture.ts +0 -33
  784. package/src/__tests__/fog-custom-render-feature.integration.test.ts +0 -76
  785. package/src/__tests__/fog-inspection.integration.test.ts +0 -127
  786. package/src/__tests__/fog-owner-state.integration.test.ts +0 -112
  787. package/src/__tests__/fog-pipeline-topology.integration.test.ts +0 -93
  788. package/src/__tests__/fog-public-surface.integration.test.ts +0 -60
  789. package/src/__tests__/fog-ray-contract.integration.test.ts +0 -73
  790. package/src/__tests__/fog-source-absence.integration.test.ts +0 -51
  791. package/src/__tests__/fog-temporal.integration.test.ts +0 -115
  792. package/src/__tests__/frame-chain.unit.test.ts +0 -26
  793. package/src/__tests__/gpu-scene-field-type-owner.test-d.ts +0 -27
  794. package/src/__tests__/hdrp-pipeline.unit.test.ts +0 -100
  795. package/src/__tests__/ibl/ibl-fallback.unit.test.ts +0 -23
  796. package/src/__tests__/material-derived-error-stop-draw.integration.test.ts +0 -69
  797. package/src/__tests__/material-derived-payload.integration.test.ts +0 -164
  798. package/src/__tests__/materials.unit.test.ts +0 -25
  799. package/src/__tests__/param-schema-projection-census.unit.test.ts +0 -40
  800. package/src/__tests__/persistent-render-scene.unit.test.ts +0 -1311
  801. package/src/__tests__/pipeline-error-policy-owner.unit.test.ts +0 -167
  802. package/src/__tests__/pipeline-pass-trace.test.ts +0 -92
  803. package/src/__tests__/pipeline-stage-owner.test-d.ts +0 -13
  804. package/src/__tests__/plugin.test.ts +0 -25
  805. package/src/__tests__/prepared-graphics-backend-matrix.unit.test.ts +0 -129
  806. package/src/__tests__/prepared-graphics-resolver-recording.unit.test.ts +0 -180
  807. package/src/__tests__/public-pipeline-id.test-d.ts +0 -6
  808. package/src/__tests__/render-dispatch-membership.unit.test.ts +0 -117
  809. package/src/__tests__/render-feature-construction.unit.test.ts +0 -124
  810. package/src/__tests__/render-feature-error-code-owner.test-d.ts +0 -27
  811. package/src/__tests__/render-feature-gpu-buffer-usage-owner.test-d.ts +0 -46
  812. package/src/__tests__/render-feature-graph.integration.test.ts +0 -205
  813. package/src/__tests__/render-feature-graphics-staging.unit.test.ts +0 -200
  814. package/src/__tests__/render-feature-order.unit.test.ts +0 -66
  815. package/src/__tests__/render-feature-prepared-graphics-recording.unit.test.ts +0 -107
  816. package/src/__tests__/render-feature-staging.unit.test.ts +0 -94
  817. package/src/__tests__/renderer-render-scene-public.test-d.ts +0 -27
  818. package/src/__tests__/scene-temporal-coverage.unit.test.ts +0 -259
  819. package/src/__tests__/sun-snapshot-consumers.test.ts +0 -35
  820. package/src/__tests__/surface-backend-kind-owner.test-d.ts +0 -42
  821. package/src/__tests__/taa-history-store.unit.test.ts +0 -196
  822. package/src/__tests__/temporal-frame-transaction.integration.test.ts +0 -537
  823. package/src/__tests__/temporal-inspection.unit.test.ts +0 -138
  824. package/src/assets/__tests__/asset-decoders.unit.test.ts +0 -92
  825. package/src/assets/__tests__/mipmap-generator.unit.test.ts +0 -52
  826. package/src/assets/__tests__/render-asset-port.unit.test.ts +0 -54
  827. package/src/assets/__tests__/resolve-render-asset.unit.test.ts +0 -47
  828. package/src/components/__tests__/atmosphere-red.test.ts +0 -51
  829. package/src/components/__tests__/atmosphere.test.ts +0 -79
  830. package/src/components/__tests__/fog.test-d.ts +0 -49
  831. package/src/components/__tests__/fog.test.ts +0 -60
  832. package/src/components/atmosphere.ts +0 -64
  833. package/src/deferred-lighting.ts +0 -92
  834. package/src/dynamic-texture-store.ts +0 -111
  835. package/src/environment/README.md +0 -76
  836. package/src/environment/__tests__/failure-injection.test.ts +0 -50
  837. package/src/environment/__tests__/generation-red.test.ts +0 -64
  838. package/src/environment/__tests__/generation.test.ts +0 -37
  839. package/src/environment/__tests__/graph-transaction.test.ts +0 -85
  840. package/src/environment/__tests__/inspection-red.test.ts +0 -53
  841. package/src/environment/__tests__/lifecycle-evidence.test.ts +0 -40
  842. package/src/environment/__tests__/pipeline-topology.test.ts +0 -33
  843. package/src/environment/__tests__/readiness.test.ts +0 -42
  844. package/src/environment/__tests__/recovery.test.ts +0 -62
  845. package/src/environment/__tests__/resize-dispose.test.ts +0 -30
  846. package/src/environment/__tests__/signature-red.test.ts +0 -45
  847. package/src/environment/__tests__/signature.test.ts +0 -60
  848. package/src/environment/descriptors.ts +0 -7
  849. package/src/environment/extracted-environment.ts +0 -239
  850. package/src/environment/generation.ts +0 -158
  851. package/src/environment/gpu-bundle.ts +0 -144
  852. package/src/environment/graph.ts +0 -588
  853. package/src/environment/inspection.ts +0 -107
  854. package/src/environment/lifecycle.ts +0 -124
  855. package/src/environment/readiness.ts +0 -76
  856. package/src/environment/signature.ts +0 -185
  857. package/src/extract/__tests__/directional-csm-domain.unit.test.ts +0 -391
  858. package/src/extract/__tests__/environment-source.test.ts +0 -105
  859. package/src/extract/__tests__/schema-readers.unit.test.ts +0 -16
  860. package/src/extract/directional-csm.ts +0 -959
  861. package/src/extract/index.ts +0 -10
  862. package/src/extract/material-interface.ts +0 -604
  863. package/src/extract/schema-readers.ts +0 -27
  864. package/src/features/graph-contribution.ts +0 -577
  865. package/src/features/index.ts +0 -100
  866. package/src/features/morph/index.ts +0 -24
  867. package/src/features/morph/morph-bounds.ts +0 -123
  868. package/src/features/scene-temporal.ts +0 -274
  869. package/src/features/taa-resolve.ts +0 -244
  870. package/src/frame-chain.ts +0 -21
  871. package/src/hdrp-cluster-membership.ts +0 -70
  872. package/src/ibl/cubemap-projection.ts +0 -60
  873. package/src/ibl/face-uniforms.ts +0 -150
  874. package/src/mesh-update-errors.ts +0 -113
  875. package/src/mipmap-generator.ts +0 -244
  876. package/src/param-schema-projection-census.ts +0 -73
  877. package/src/persistent-render-scene.ts +0 -1752
  878. package/src/pipeline/index.ts +0 -40
  879. package/src/pipeline-errors.ts +0 -172
  880. package/src/prepare/__tests__/schema-readers.unit.test.ts +0 -16
  881. package/src/prepare/index.ts +0 -8
  882. package/src/prepare/schema-readers.ts +0 -20
  883. package/src/record/__tests__/environment-record.test.ts +0 -35
  884. package/src/record/__tests__/frame-targets.unit.test.ts +0 -54
  885. package/src/record/__tests__/membership-timing.unit.test.ts +0 -524
  886. package/src/record/__tests__/render-submission.integration.test.ts +0 -76
  887. package/src/record/__tests__/render-submission.perf.test.ts +0 -141
  888. package/src/record/__tests__/shadow-submit-mode.unit.test.ts +0 -88
  889. package/src/record/__tests__/view-ubo-layout.unit.test.ts +0 -101
  890. package/src/record/frame-observation.ts +0 -122
  891. package/src/record/index.ts +0 -15
  892. package/src/record/main-pass-skin.ts +0 -51
  893. package/src/record/main-pass-sprite.ts +0 -166
  894. package/src/record/material-payload.ts +0 -292
  895. package/src/record/material-slot-plan.ts +0 -68
  896. package/src/record/membership-timing.ts +0 -1012
  897. package/src/record/shadow-caster-uniform.ts +0 -37
  898. package/src/record/shadow-submit-mode.ts +0 -277
  899. package/src/render-dispatch-membership.ts +0 -155
  900. package/src/render-pipeline-context.ts +0 -332
  901. package/src/render-pipeline-target.ts +0 -96
  902. package/src/render-scene-projection.ts +0 -1738
  903. package/src/render-system-fold.ts +0 -531
  904. package/src/renderer/asset-service.ts +0 -32
  905. package/src/renderer/backend-selection.ts +0 -121
  906. package/src/renderer/environment-classify.ts +0 -30
  907. package/src/renderer/environment-error.ts +0 -19
  908. package/src/renderer/material/pipeline-projection.ts +0 -140
  909. package/src/renderer.ts +0 -940
  910. package/src/scene-utils/collect-subtree.ts +0 -40
  911. package/src/ssao-data.ts +0 -106
  912. package/src/surface-format.ts +0 -37
  913. package/src/taa-history-store.ts +0 -429
  914. package/src/temporal-coverage.ts +0 -106
  915. package/src/temporal-frame-transaction.ts +0 -301
  916. package/src/temporal-inspection.ts +0 -194
  917. package/src/temporal-view.ts +0 -174
  918. 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,36 +142,21 @@ 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,
173
162
  MeshFilter,
@@ -180,327 +169,33 @@ import {
180
169
  SpotLight,
181
170
  SpriteInstances,
182
171
  SpriteRegionOverride,
183
- type Tonemap,
184
172
  tonemapFromF32,
185
173
  tonemapToU32,
186
174
  } from './components';
187
175
  import { GlyphText } from './components/glyph-text';
176
+ import { computeInvRangeSquared, degToCos } from './components/light-helpers';
188
177
  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
178
  MaterialSkinAttrMissingError,
204
- type RenderError,
179
+ ShadowInvalidConfigError,
205
180
  SkinMaterialMismatchError,
206
181
  } 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
182
  import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
247
183
  import type {
248
184
  RenderFeatureHiddenEntityReport,
249
185
  RenderFeatureWorldVisibilitySnapshot,
250
186
  } from './features/types';
251
187
  import { ensureGlyphMeshMaterialSlots } from './glyph-text-layout-system';
252
- import type { RenderSceneFactInput } from './gpu-scene-schema';
253
188
  import {
254
189
  type MeshMaterialBindingDiagnostic,
255
190
  type MeshMaterialBindingSource,
256
191
  resolveMeshMaterialBindings,
257
192
  } from './mesh-material-bindings';
193
+ import { isStandardPbrMaterialShader } from './pbr-pipeline';
194
+ import { type CameraSnapshot, STANDARD_TONEMAP_FEATURE_ID } from './render-contract';
258
195
  import { getActiveCamera, selectActiveCameraIndex } from './systems/active-camera';
259
196
  import { selectPasses } from './systems/pass-selector';
260
197
  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
- }
198
+ import type { SkinPaletteSlice } from './systems/skin-palette-types';
504
199
 
505
200
  /**
506
201
  * DirectionalLightSnapshot — sun-like infinite light variant of the
@@ -613,8 +308,6 @@ export interface PointShadowSnapshot {
613
308
  */
614
309
  export interface SpotLightSnapshot {
615
310
  readonly kind: 'spot';
616
- /** ECS entity handle in the source World; diagnostic identity only. */
617
- readonly entity: number;
618
311
  readonly position: Vec3;
619
312
  readonly direction: Vec3;
620
313
  readonly color: Vec3;
@@ -744,10 +437,6 @@ export interface ExtractedLights {
744
437
  * when castShadow=false or no directional light.
745
438
  */
746
439
  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
440
  }
752
441
 
753
442
  /**
@@ -757,7 +446,7 @@ export interface ExtractedLights {
757
446
  * `equirectHandle` carries the packed u32 handle for
758
447
  * `Handle<EquirectAsset, 'shared'>`; the record stage drives the internal
759
448
  * lazy cubemap projection from it and resolves the projected GPU cubemap via
760
- * `GpuResourceStore.getCubemapGpuView(...)` and related helpers
449
+ * `GpuResidencyCache.getCubemapGpuView(...)` and related helpers
761
450
  * (feat-20260630 M3 / w16-w18). The snapshot carries NO projection status
762
451
  * field: the status truth lives in the store's CubemapGpuEntry (D-3 SSOT);
763
452
  * record queries it once per frame.
@@ -781,128 +470,13 @@ export interface SkylightSnapshot {
781
470
  readonly entityHandle: number;
782
471
  }
783
472
 
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
473
  /**
900
474
  * SkyboxSnapshot -- extract-stage view of one SkyboxBackground entity
901
475
  * (feat-20260531-skybox-env-background M2 / w5).
902
476
  *
903
477
  * `equirectHandle` carries the packed u32 handle for
904
478
  * `Handle<EquirectAsset, 'shared'>`; the record stage resolves the projected
905
- * GPU cubemap via `GpuResourceStore.getCubemapGpuView(...)`.
479
+ * GPU cubemap via `GpuResidencyCache.getCubemapGpuView(...)`.
906
480
  *
907
481
  * `mode` carries the raw `f32` column value (`SKYBOX_MODE_CUBEMAP = 0`).
908
482
  * First hit wins per plan-strategy D-6; multi-entity once-warn in record stage.
@@ -921,27 +495,9 @@ export interface SkyboxSnapshot {
921
495
  export interface RenderableSnapshot {
922
496
  readonly assetHandle: number;
923
497
  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
498
  /** Finite producer-owned local bounds retained for persistent CPU view work. */
934
499
  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
- */
500
+ /** Indexed single-submesh facts consumed by the first GPU-driven rigid lane. */
945
501
  readonly gpuDrivenDraws?: readonly GpuDrivenDrawSnapshot[];
946
502
  /**
947
503
  * feat-20260608 M5 amend / w11-a: the entity's representative (first)
@@ -997,8 +553,6 @@ export interface RenderableSnapshot {
997
553
  * not the bare entityKey alone (D-1).
998
554
  */
999
555
  readonly entityKey: number;
1000
- /** Stable pre-main-cull candidate index used by the directional CSM plan. */
1001
- readonly candidateIndex?: number;
1002
556
  /**
1003
557
  * feat-20260514 M3 (w15): when the entity carries an `Instances` component
1004
558
  * the extract stage materialises a fresh `Float32Array` snapshot of the
@@ -1038,41 +592,20 @@ export interface RenderableSnapshot {
1038
592
  * Absent (`undefined`) means the entity is not skinned.
1039
593
  */
1040
594
  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;
595
+ /**
596
+ * ECS-owned morph weights. Presence selects the Standard Pipeline CPU
597
+ * specialized deformation lane; the asset remains the source of target
598
+ * deltas and the record stage only receives this frozen POD snapshot.
599
+ */
600
+ readonly morph?: MorphSnapshot;
1045
601
  }
1046
602
 
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;
603
+ export interface MorphSnapshot {
604
+ readonly weights: Float32Array;
605
+ readonly targetCount: number;
1067
606
  }
1068
607
 
1069
608
  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
609
  readonly kind: 'indexed' | 'non-indexed';
1077
610
  readonly first: number;
1078
611
  readonly count: number;
@@ -1083,41 +616,6 @@ export interface GpuDrivenDrawSnapshot {
1083
616
  readonly materialResourceClass: string;
1084
617
  }
1085
618
 
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
619
  export interface InstancesSnapshot {
1122
620
  /** Packed column-major mat4 transforms (16 f32 per instance). */
1123
621
  readonly transforms: Float32Array;
@@ -1177,56 +675,6 @@ export interface TransformSnapshot {
1177
675
  readonly world: Float32Array;
1178
676
  }
1179
677
 
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
678
  /**
1231
679
  * MaterialSnapshot: extract-stage view of one entity's material asset (M2 / w6
1232
680
  * of feat-20260517-merge-mesh-renderer-material-renderer; plan-strategy section 2.3).
@@ -1267,16 +715,12 @@ export interface MaterialSnapshot {
1267
715
  * discriminator (M4-T06).
1268
716
  */
1269
717
  readonly materialShaderId?: string | undefined;
1270
- /** Canonical RenderAssetPort GUID used to join the production readiness tuple. */
1271
- readonly materialGuid?: string | undefined;
1272
718
  /**
1273
719
  * Stable source handle for the material asset. Record uses this only for
1274
720
  * same-frame assembly reuse; GPU bind-group validity still comes from the
1275
721
  * resource-identity cache.
1276
722
  */
1277
723
  readonly materialHandle?: number | undefined;
1278
- /** Whether the resolved material owns a ShadowCaster pass. */
1279
- readonly shadowCaster?: boolean | undefined;
1280
724
  /**
1281
725
  * The selected forward-pass state for this exact material. A mesh can bind
1282
726
  * different materials to its submeshes, so pipeline selection must read the
@@ -1292,10 +736,6 @@ export interface MaterialSnapshot {
1292
736
  readonly paramSnapshot?: Readonly<Record<string, number | number[] | string>> | undefined;
1293
737
  /** Effective MaterialAsset parameter contract used to lay out this snapshot. */
1294
738
  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
739
  /** Authored per-slot UV set and KHR texture transform metadata. */
1300
740
  readonly textureCoordinates?: ReadonlyMap<string, MaterialTextureCoordinates> | undefined;
1301
741
  /**
@@ -1303,7 +743,7 @@ export interface MaterialSnapshot {
1303
743
  * (feat-20260621-learn-render-5-5-parallax M2 / w7). The SSOT carrier for
1304
744
  * EVERY texture the shader's `derive(paramSchema).textureFieldNames`
1305
745
  * declares — `baseColorTexture` / `metallicRoughnessTexture` /
1306
- * `normalTexture` for built-in standard-PBR, plus any custom field such as
746
+ * `normalTexture` for built-in standard-PBR, plus each custom field such as
1307
747
  * `heightTexture` (LO 5.5 parallax). The record stage iterates this map to
1308
748
  * assemble the user-region bind group per the per-shader BGL (w8), so a 4th
1309
749
  * (or Nth) texture flows end-to-end without a hardcoded field list.
@@ -1322,7 +762,7 @@ export interface MaterialSnapshot {
1322
762
  * occupies the same texture2d values slot a static texture would (P4:
1323
763
  * one binding shape), but extract routes it here instead of `textureHandles`
1324
764
  * so the record stage pulls the current-frame view from the transient
1325
- * DynamicTextureStore (D-3) instead of `GpuResourceStore.ensureResident`
765
+ * DynamicTextureStore (D-3) instead of `GpuResidencyCache.ensureResident`
1326
766
  * (which has no `video` arm; AC-08). Each entry also carries the resolved
1327
767
  * clip handle so the record stage can key the per-frame upload.
1328
768
  *
@@ -1403,7 +843,7 @@ export interface MaterialSnapshotCacheEntry {
1403
843
  readonly passes: readonly MaterialPass[];
1404
844
  /** Root/chain identities are present only on cross-frame-safe entries. */
1405
845
  readonly chain?: readonly MaterialCacheChainIdentity[];
1406
- /** RenderAssetPort epoch at which the root/parent chain was last validated. */
846
+ /** AssetRegistry epoch at which the root/parent chain was last validated. */
1407
847
  readonly catalogEpoch?: number;
1408
848
  /** True only when every object reachable from the authored chain is frozen. */
1409
849
  readonly crossFrameSafe: boolean;
@@ -1426,39 +866,6 @@ function gpuDrivenMaterialResourceClass(material: MaterialSnapshot): string {
1426
866
  });
1427
867
  }
1428
868
 
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
869
  // === DispatchEntry — M3 / w26 single dispatch list (feat-20260526-material-asset-multipass-renderstate) ===
1463
870
  //
1464
871
  // Plan-strategy D-3: single dispatch list sorted by queue value,
@@ -1507,6 +914,12 @@ export function sortDispatchByQueue<E extends { readonly queue: number }>(
1507
914
  return entries.slice().sort((a, b) => a.queue - b.queue);
1508
915
  }
1509
916
 
917
+ const DEFAULT_FORWARD_PASS: MaterialPass = {
918
+ name: 'forward',
919
+ program: { module: 'forgeax::default-unlit', vertexEntry: 'vs_main', fragmentEntry: 'fs_main' },
920
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
921
+ };
922
+
1510
923
  function appendMaterialDispatchEntries(
1511
924
  pendingDispatch: DispatchEntry[],
1512
925
  passes: readonly MaterialPass[],
@@ -1515,6 +928,7 @@ function appendMaterialDispatchEntries(
1515
928
  renderableIndex: number,
1516
929
  layer: number,
1517
930
  paramSnapshot: Readonly<Record<string, number | number[] | string>> | undefined,
931
+ passIndexOffset = 0,
1518
932
  ): void {
1519
933
  const matchedPasses = selectPasses(passes, {});
1520
934
  for (let pIdx = 0; pIdx < matchedPasses.length; pIdx++) {
@@ -1529,8 +943,8 @@ function appendMaterialDispatchEntries(
1529
943
  entityIndex: entity,
1530
944
  materialHandle,
1531
945
  renderableIndex,
1532
- passIndex: pIdx,
1533
- queue: passState.queue ?? RenderQueue.Geometry,
946
+ passIndex: passIndexOffset + pIdx,
947
+ queue: passState.queue ?? 2000,
1534
948
  layer,
1535
949
  tags: passState.tags ?? {},
1536
950
  renderState: pipelineRenderState(passState),
@@ -1550,23 +964,16 @@ export interface ExtractedFrame {
1550
964
  readonly cameras: CameraSnapshot[];
1551
965
  readonly lights: ExtractedLights;
1552
966
  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
967
  /**
1564
968
  * Single dispatch list sorted by queue value (ascending, stable sort).
1565
969
  * Replaces the old opaqueDispatch / transparentDispatch / overlayDispatch
1566
970
  * three-bucket model per plan-strategy D-3.
1567
971
  */
1568
972
  readonly dispatch: DispatchEntry[];
1569
- readonly environment: ExtractedEnvironment;
973
+ readonly skylight: SkylightSnapshot | undefined;
974
+ readonly skylightCount: number;
975
+ readonly skybox: SkyboxSnapshot | undefined;
976
+ readonly skyboxCount: number;
1570
977
  /**
1571
978
  * feat-20260528-frustum-culling M3 / w11: frustum culling statistics
1572
979
  * collected during the extract phase. `total` is the count of entities
@@ -1593,53 +1000,82 @@ export interface ExtractedFrame {
1593
1000
  }
1594
1001
 
1595
1002
  /**
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).
1003
+ * Internal world surface used by extract for archetype-graph traversal and
1004
+ * error routing. The packed `Entity` handle for a row is read directly from
1005
+ * the essential id=0 `Entity` column (`arch.columns.get(Entity.id).get('self')`)
1006
+ * -- no generation lookup / encodeEntity rebuild (feat-20260602 M2).
1616
1007
  */
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;
1008
+ type RenderErrorContext = {
1009
+ readonly systemName: string;
1010
+ /** Retained as diagnostic metadata for legacy render call sites. */
1011
+ readonly severity?: 'error' | 'warning';
1012
+ };
1013
+
1014
+ type ArrayFieldView = ArrayLike<number>;
1015
+
1016
+ /**
1017
+ * Render's old zero-copy ECS calls now use the explicit public projection
1018
+ * boundary. The optimized ECS owner keeps storage behind `World`; render
1019
+ * receives a detached array snapshot and routes failures through the same
1020
+ * projection owner.
1021
+ */
1022
+ type WorldInternalView = {
1023
+ _routeError(error: unknown, ctx: RenderErrorContext): void;
1024
+ _getArrayView<N extends string, S extends ComponentSchema>(
1025
+ entity: EntityHandle,
1026
+ component: Component<N, S>,
1027
+ fieldName: string,
1028
+ ): ArrayFieldView | undefined;
1029
+ };
1030
+
1031
+ const Severity = Object.freeze({ Error: 'error', Warning: 'warning' } as const);
1032
+
1033
+ function createWorldInternalView(world: World): WorldInternalView {
1034
+ return {
1035
+ _routeError(error, ctx) {
1036
+ routeWorldError(world, error, { systemName: ctx.systemName });
1037
+ },
1038
+ _getArrayView(entity, component, fieldName) {
1039
+ return readRenderArrayView(world, entity, component, fieldName) as ArrayFieldView | undefined;
1040
+ },
1041
+ };
1042
+ }
1043
+
1044
+ /**
1045
+ * The user-region texture fields the built-in standard-PBR material declares.
1046
+ * Used as the fallback texture-field set when the shader id is not registered
1047
+ * (cross-worktree shader-late-register, plan R-4) so a built-in material still
1048
+ * resolves its user-region textures. Mirrors `derive(default-standard-pbr).textureFieldNames`.
1049
+ */
1050
+ const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
1051
+ 'baseColorTexture',
1052
+ 'metallicRoughnessTexture',
1053
+ 'normalTexture',
1054
+ 'specularTintTexture',
1055
+ ];
1056
+ const BUILTIN_USER_REGION_TEXTURE_FIELD_SET = new Set(BUILTIN_USER_REGION_TEXTURE_FIELDS);
1057
+ const BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET = new Set(['baseColorTexture']);
1058
+
1059
+ function materialTextureFields(
1060
+ shaderId: string | undefined,
1061
+ fields: ReadonlySet<string> | undefined,
1062
+ ): ReadonlySet<string> | undefined {
1063
+ if (fields !== undefined && fields.size > 0) return fields;
1064
+ if (
1065
+ shaderId === 'forgeax::default-standard-pbr' ||
1066
+ shaderId === 'forgeax::default-standard-pbr-skin' ||
1067
+ shaderId === 'forgeax::pbr-skin'
1068
+ ) {
1069
+ return BUILTIN_USER_REGION_TEXTURE_FIELD_SET;
1070
+ }
1071
+ if (
1072
+ shaderId === 'forgeax::default-unlit' ||
1073
+ shaderId === 'forgeax::sprite' ||
1074
+ shaderId === 'forgeax::sprite-lit'
1075
+ ) {
1076
+ return BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET;
1077
+ }
1078
+ return fields;
1643
1079
  }
1644
1080
 
1645
1081
  /**
@@ -1724,12 +1160,12 @@ function pipelineRenderState(
1724
1160
 
1725
1161
  /**
1726
1162
  * 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
1163
+ * the loadByGuid texture/sampler resolution path. A MaterialAsset's
1728
1164
  * 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
1165
+ * loadByGuid; the extract stage re-resolves each GUID to a column handle every
1730
1166
  * frame. Before this cache each resolution called `world.allocSharedRef`, which
1731
1167
  * mints a NEW monotonically-increasing slot id per call. Because the GPU
1732
- * residency cache (`GpuResourceStore.textureGpuHandles`) is keyed on
1168
+ * residency cache (`GpuResidencyCache.textureGpuHandles`) is keyed on
1733
1169
  * `handleSlot(handle)`, a fresh slot every frame meant the residency check
1734
1170
  * ALWAYS missed -> all textures re-uploaded to the GPU every frame, old GPU
1735
1171
  * textures never freed (refcount never hits 0). Unbounded GPU memory +
@@ -1740,11 +1176,10 @@ function pipelineRenderState(
1740
1176
  * across frames (architecture-principle §6 idempotency: same GUID resolved N
1741
1177
  * times yields the same handle). The handle is intentionally long-lived -- it
1742
1178
  * 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.
1179
+ * desired lifetime; the `onLastRelease` -> `gpuStore.evictTexture` wiring stays
1180
+ * coherent because the handle is no longer churned per frame.
1746
1181
  *
1747
- * Invariant boundary (asset-registry.ts:1958-1962): the RenderAssetPort is a
1182
+ * Invariant boundary (asset-registry.ts:1958-1962): the AssetRegistry is a
1748
1183
  * GUID -> payload catalogue with NO handle/World concept -- it cannot mint a
1749
1184
  * column handle. So this intern cache lives in the extract/render layer, keyed
1750
1185
  * per-World via a WeakMap (the World owns the SharedRefStore that mints slots).
@@ -1767,7 +1202,7 @@ const guidHandleInternByWorld = new WeakMap<World, Map<string, GuidHandleInternE
1767
1202
 
1768
1203
  function internSharedRefFromGuid<B extends string>(
1769
1204
  world: World,
1770
- assetsRef: RenderAssetPort,
1205
+ assetsRef: AssetRegistry,
1771
1206
  guid: string,
1772
1207
  brand: B,
1773
1208
  ): Handle<B, 'shared'> | undefined {
@@ -1780,19 +1215,19 @@ function internSharedRefFromGuid<B extends string>(
1780
1215
  const cached = perWorld.get(key);
1781
1216
  const payload = assetsRef.lookup(guid);
1782
1217
  if (cached !== undefined && cached.payload === payload) {
1783
- const cachedHandle = cached.handle as unknown as Handle<B, 'shared'>;
1218
+ const cachedHandle = toShared<B>(cached.handle);
1784
1219
  if (world.sharedRefs.resolve(cachedHandle).ok) return cachedHandle;
1785
1220
  }
1786
1221
  if (cached !== undefined) {
1787
- world.sharedRefs.release(cached.handle as unknown as Handle<B, 'shared'>);
1222
+ world.sharedRefs.release(toShared<B>(cached.handle));
1788
1223
  perWorld.delete(key);
1789
1224
  }
1790
1225
  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 });
1226
+ // Mint exactly once per (world, guid, brand). ECS owns the allocation grant
1227
+ // and publishes release evidence through SharedRefStore; render does not
1228
+ // attach a second deleter lifecycle to the World.
1229
+ const handle = world.internSharedRef(brand, payload);
1230
+ perWorld.set(key, { handle, payload });
1796
1231
  return handle;
1797
1232
  }
1798
1233
 
@@ -1817,7 +1252,7 @@ function internSharedRefFromGuid<B extends string>(
1817
1252
  function resolveVideoFieldHandle(
1818
1253
  value: unknown,
1819
1254
  world: World,
1820
- assetsRef: RenderAssetPort,
1255
+ assetsRef: AssetRegistry,
1821
1256
  ): Handle<'VideoAsset', 'shared'> | undefined {
1822
1257
  const texture = materialTextureValue(value);
1823
1258
  const textureGuid = assetReferenceText(texture?.texture ?? value);
@@ -1843,11 +1278,14 @@ function materialTextureValue(value: unknown): MaterialTextureValue | undefined
1843
1278
 
1844
1279
  function assetReferenceText(value: unknown): string | undefined {
1845
1280
  if (typeof value === 'string') return value;
1846
- if (value instanceof Uint8Array && value.byteLength === 16)
1847
- return AssetGuid.format(value as never);
1281
+ if (value instanceof Uint8Array && isAssetGuidBytes(value)) return AssetGuid.format(value);
1848
1282
  return undefined;
1849
1283
  }
1850
1284
 
1285
+ function isAssetGuidBytes(value: Uint8Array): value is AssetGuidBytes {
1286
+ return value.byteLength === 16;
1287
+ }
1288
+
1851
1289
  function materialNormalScale(values: Readonly<Record<string, unknown>>): number {
1852
1290
  for (const [field, value] of Object.entries(values)) {
1853
1291
  if (field !== 'normalTexture') continue;
@@ -1863,14 +1301,16 @@ function materialTextureRef(value: unknown): unknown {
1863
1301
 
1864
1302
  function collectMaterialTextureCoordinates(
1865
1303
  values: Readonly<Record<string, unknown>>,
1866
- schema: readonly ParamSchemaEntry[],
1867
- derivedInterface?: DerivedMaterialInterface,
1868
- paramSchemaProjectionCensus?: ParamSchemaProjectionCensus,
1869
1304
  ): Map<string, MaterialTextureCoordinates> {
1870
1305
  const out = new Map<string, MaterialTextureCoordinates>();
1871
- for (const field of (
1872
- derivedInterface ?? deriveParamSchemaForCensus(schema, paramSchemaProjectionCensus, 'submesh')
1873
- ).textureFieldNames) {
1306
+ for (const field of [
1307
+ 'baseColorTexture',
1308
+ 'metallicRoughnessTexture',
1309
+ 'normalTexture',
1310
+ 'specularTintTexture',
1311
+ 'emissiveTexture',
1312
+ 'occlusionTexture',
1313
+ ]) {
1874
1314
  const coordinates = materialTextureValue(values[field])?.coordinates;
1875
1315
  if (coordinates !== undefined) out.set(field, coordinates);
1876
1316
  }
@@ -1897,14 +1337,14 @@ function collectMaterialTextureSamplers(
1897
1337
  * feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): collect the
1898
1338
  * user-region texture handles for a material by iterating the shader's
1899
1339
  * `derive(paramSchema).textureFieldNames` SSOT (via
1900
- * `RenderAssetPort.materialShaderTextureFieldNames`). Each declared texture
1340
+ * `AssetRegistry.materialShaderTextureFieldNames`). Each declared texture
1901
1341
  * 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
1342
+ * by field name; this is the single path through which an arbitrary number of user-region
1903
1343
  * textures (4 standard or 5+ custom, e.g. parallax `heightTexture`) flow.
1904
1344
  *
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.
1345
+ * When the shader is not registered the built-in 4-field set is used so
1346
+ * standard materials still resolve. `emissiveTexture` / `occlusionTexture`
1347
+ * are engine-injection (lightmap) textures and are NOT part of this set.
1908
1348
  *
1909
1349
  * feat-20260623-world-space-video-asset M4 / w14 (D-5): a field whose paramValue
1910
1350
  * catalogues to a VideoAsset is routed into `videoOut` (a VideoAsset-branded
@@ -1918,8 +1358,7 @@ function collectUserRegionTextureHandles(
1918
1358
  pv: Readonly<Record<string, unknown>>,
1919
1359
  shaderId: string | undefined,
1920
1360
  paramSchema: readonly ParamSchemaEntry[] | undefined,
1921
- derivedInterface: DerivedMaterialInterface | undefined,
1922
- assetsRef: RenderAssetPort,
1361
+ assetsRef: AssetRegistry,
1923
1362
  world: World,
1924
1363
  resolveTex: (
1925
1364
  value: unknown,
@@ -1928,38 +1367,16 @@ function collectUserRegionTextureHandles(
1928
1367
  videoOut: Map<string, Handle<'VideoAsset', 'shared'>>,
1929
1368
  ): Map<string, Handle<'TextureAsset', 'shared'>> {
1930
1369
  const fields =
1931
- derivedInterface !== undefined
1932
- ? derivedInterface.textureFieldNames
1933
- : paramSchema !== undefined && paramSchema.length > 0
1370
+ materialTextureFields(
1371
+ shaderId,
1372
+ paramSchema !== undefined
1934
1373
  ? 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;
1374
+ : shaderId !== undefined
1375
+ ? assetsRef.materialShaderTextureFieldNames(shaderId)
1376
+ : undefined,
1377
+ ) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
1961
1378
  const out = new Map<string, Handle<'TextureAsset', 'shared'>>();
1962
- for (const field of fields ?? []) {
1379
+ for (const field of fields) {
1963
1380
  // D-5: a video-kind paramValue is routed to the transient path (videoOut),
1964
1381
  // NOT minted as a TextureAsset (which would crash the record-stage
1965
1382
  // ensureResident, AC-08). A static field falls through to resolveTex.
@@ -1974,34 +1391,32 @@ function collectUserRegionTextureHandles(
1974
1391
  return out;
1975
1392
  }
1976
1393
 
1394
+ const ENGINE_INJECTED_TEXTURE_FIELDS = new Set(['emissiveTexture', 'occlusionTexture']);
1395
+
1396
+ function isEngineInjectedTextureField(shaderId: string | undefined, fieldName: string): boolean {
1397
+ return isStandardPbrMaterialShader(shaderId) && ENGINE_INJECTED_TEXTURE_FIELDS.has(fieldName);
1398
+ }
1399
+
1977
1400
  function materialParametersToParamSchema(
1978
1401
  parameters: readonly MaterialParameter[],
1979
1402
  shaderId: string | undefined,
1980
- assets: RenderAssetPort,
1981
1403
  ): 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
- }
1404
+ const engineInjectedTextureFields = isStandardPbrMaterialShader(shaderId)
1405
+ ? ENGINE_INJECTED_TEXTURE_FIELDS
1406
+ : undefined;
1986
1407
  return parameters.flatMap((parameter): ParamSchemaEntry[] => {
1987
1408
  // Static values select a cooked specialization and boolean material
1988
1409
  // values have no runtime UBO representation in ParamSchemaEntry.
1989
1410
  if (parameter.static || parameter.type === 'bool') return [];
1990
1411
  if (parameter.type === 'texture') {
1412
+ if (engineInjectedTextureFields?.has(parameter.name) === true) return [];
1991
1413
  return [{ name: parameter.name, type: 'texture2d' }];
1992
1414
  }
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
1415
  return [
2000
1416
  {
2001
1417
  name: parameter.name,
2002
1418
  type: parameter.type,
2003
1419
  ...(parameter.colorSpace === undefined ? {} : { colorSpace: parameter.colorSpace }),
2004
- ...numericDefault,
2005
1420
  },
2006
1421
  ];
2007
1422
  });
@@ -2016,7 +1431,7 @@ function materialParametersToParamSchema(
2016
1431
  function materialColorParameterSchema(
2017
1432
  parameters: readonly MaterialParameter[],
2018
1433
  shaderId: string | undefined,
2019
- assets: RenderAssetPort,
1434
+ assets: AssetRegistry,
2020
1435
  ): readonly MaterialColorParameterSchema[] {
2021
1436
  if (shaderId === undefined) return parameters;
2022
1437
  const shader = assets.shaderRegistry.findMaterialArtifact(shaderId);
@@ -2044,13 +1459,13 @@ function isDeepFrozen(value: unknown, seen = new Set<object>()): boolean {
2044
1459
 
2045
1460
  /**
2046
1461
  * Capture the object identities that make a resolved material stable. The
2047
- * renderer owns this cache, but the RenderAssetPort owns parent GUID lookup, so
1462
+ * renderer owns this cache, but the AssetRegistry owns parent GUID lookup, so
2048
1463
  * validation must walk the same chain on every cross-frame read. A malformed
2049
1464
  * or cyclic chain is deliberately not cacheable.
2050
1465
  */
2051
1466
  function captureMaterialCacheChain(
2052
1467
  root: MaterialAsset,
2053
- assets: RenderAssetPort,
1468
+ assets: AssetRegistry,
2054
1469
  ): readonly MaterialCacheChainIdentity[] | undefined {
2055
1470
  const chain: MaterialCacheChainIdentity[] = [];
2056
1471
  const visitedParentGuids = new Set<string>();
@@ -2063,7 +1478,7 @@ function captureMaterialCacheChain(
2063
1478
  if (parentRef === undefined) return undefined;
2064
1479
  if (visitedParentGuids.has(parentGuid)) return undefined;
2065
1480
  visitedParentGuids.add(parentGuid);
2066
- const parent = assets.lookup<Asset>(parentGuid);
1481
+ const parent = assets.lookup<Asset>(parentRef);
2067
1482
  if (parent?.kind !== 'material') return undefined;
2068
1483
  current = parent;
2069
1484
  }
@@ -2073,11 +1488,15 @@ function captureMaterialCacheChain(
2073
1488
  function readPersistentMaterialSnapshot(
2074
1489
  cache: MaterialSnapshotCache | undefined,
2075
1490
  handleRaw: number,
2076
- assets: RenderAssetPort,
1491
+ assets: AssetRegistry,
2077
1492
  ): MaterialSnapshotCacheEntry | undefined {
2078
1493
  const entry = cache?.get(handleRaw);
2079
1494
  if (entry === undefined) return undefined;
2080
- if (entry.crossFrameSafe && entry.chain !== undefined && entry.catalogEpoch === assets.epoch)
1495
+ if (
1496
+ entry.crossFrameSafe &&
1497
+ entry.chain !== undefined &&
1498
+ entry.catalogEpoch === assets.catalogEpoch
1499
+ )
2081
1500
  return entry;
2082
1501
  // The snapshot contains resolved texture and sampler handles as well as the
2083
1502
  // material parent chain. Any registry mutation can replace one of those
@@ -2096,12 +1515,12 @@ function readPersistentMaterialSnapshot(
2096
1515
  function readStablePersistentMaterialSnapshot(
2097
1516
  cache: MaterialSnapshotCache | undefined,
2098
1517
  handleRaw: number,
2099
- assets: RenderAssetPort,
1518
+ assets: AssetRegistry,
2100
1519
  ): MaterialSnapshotCacheEntry | undefined {
2101
1520
  const entry = cache?.get(handleRaw);
2102
1521
  return entry?.crossFrameSafe === true &&
2103
1522
  entry.chain !== undefined &&
2104
- entry.catalogEpoch === assets.epoch
1523
+ entry.catalogEpoch === assets.catalogEpoch
2105
1524
  ? entry
2106
1525
  : undefined;
2107
1526
  }
@@ -2112,7 +1531,7 @@ function storeMaterialSnapshot(
2112
1531
  snapshot: MaterialSnapshot,
2113
1532
  passes: readonly MaterialPass[],
2114
1533
  root: MaterialAsset,
2115
- assets: RenderAssetPort,
1534
+ assets: AssetRegistry,
2116
1535
  ): MaterialSnapshotCacheEntry {
2117
1536
  const chain = captureMaterialCacheChain(root, assets);
2118
1537
  const entry: MaterialSnapshotCacheEntry = {
@@ -2120,7 +1539,7 @@ function storeMaterialSnapshot(
2120
1539
  passes,
2121
1540
  crossFrameSafe: chain?.every(({ asset }) => isDeepFrozen(asset)) === true,
2122
1541
  ...(chain === undefined ? {} : { chain }),
2123
- ...(chain === undefined ? {} : { catalogEpoch: assets.epoch }),
1542
+ ...(chain === undefined ? {} : { catalogEpoch: assets.catalogEpoch }),
2124
1543
  };
2125
1544
  cache.set(handleRaw, entry);
2126
1545
  return entry;
@@ -2136,124 +1555,211 @@ function storeMaterialSnapshot(
2136
1555
  * UBO upload, so this helper produces a plain MaterialSnapshot only).
2137
1556
  *
2138
1557
  * Returns `defaultMaterialSnapshot()` (mid-grey unlit) for handle=0 (case-B
2139
- * sentinel, mirroring the inline path) and on any unresolved / non-material
1558
+ * sentinel, mirroring the inline path) and on each unresolved / non-material
2140
1559
  * asset (so a partially-mis-registered multi-material entity still renders
2141
1560
  * the resolvable submeshes; the count-mismatch validator already filtered
2142
1561
  * the count-disagreement case earlier).
2143
1562
  */
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,
1563
+ export function resolveMaterialSnapshot(
1564
+ handleRaw: number,
2191
1565
  world: World,
2192
- assets: RenderAssetPort,
2193
- gpuStore?: import('./gpu-resource-store').GpuResourceStore,
1566
+ assetsRef: AssetRegistry,
1567
+ materialSnapshotCache?: MaterialSnapshotCache,
2194
1568
  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(),
1569
+ ): MaterialSnapshot {
1570
+ if (handleRaw === 0) return defaultMaterialSnapshot(handleRaw);
1571
+ const cached = materialSnapshotCache?.get(handleRaw);
1572
+ if (cached !== undefined) return cached.snapshot;
1573
+ const stablePersistentCached = readStablePersistentMaterialSnapshot(
2209
1574
  persistentMaterialSnapshotCache,
2210
- paramSchemaProjectionCensus,
1575
+ handleRaw,
1576
+ assetsRef,
2211
1577
  );
2212
- const materials = renderable.materials.map((material) =>
2213
- (material.materialHandle ?? 0) === materialHandle ? refreshed : material,
1578
+ if (stablePersistentCached !== undefined) {
1579
+ materialSnapshotCache?.set(handleRaw, stablePersistentCached);
1580
+ return stablePersistentCached.snapshot;
1581
+ }
1582
+ const tagged = toShared<'MaterialAsset'>(handleRaw);
1583
+ const res = resolveAssetHandle(world, tagged);
1584
+ if (!res.ok) return defaultMaterialSnapshot(handleRaw);
1585
+ const asset = res.value;
1586
+ if (asset.kind !== 'material') return defaultMaterialSnapshot(handleRaw);
1587
+ const persistentCached = readPersistentMaterialSnapshot(
1588
+ persistentMaterialSnapshotCache,
1589
+ handleRaw,
1590
+ assetsRef,
2214
1591
  );
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;
1592
+ if (persistentCached !== undefined) {
1593
+ materialSnapshotCache?.set(handleRaw, persistentCached);
1594
+ return persistentCached.snapshot;
1595
+ }
1596
+ const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assetsRef);
1597
+ if (!resolvedResult.ok) return defaultMaterialSnapshot(handleRaw);
1598
+ const resolved = resolvedResult.value;
1599
+ const allPasses = resolved.passes;
1600
+ const firstPassShader =
1601
+ allPasses.length > 0
1602
+ ? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
1603
+ : undefined;
1604
+ const pv = materialValuesToLinearRuntime(
1605
+ resolved.values,
1606
+ materialColorParameterSchema(resolved.parameters ?? [], firstPassShader, assetsRef),
1607
+ resolved.colorSpace,
1608
+ ) as Readonly<Record<string, unknown>>;
1609
+ const baseColorPv = pv.baseColor as readonly number[] | undefined;
1610
+ const baseColor = vec3.create(
1611
+ baseColorPv?.[0] ?? 1,
1612
+ baseColorPv?.[1] ?? 1,
1613
+ baseColorPv?.[2] ?? 1,
1614
+ );
1615
+ const metallicPv = typeof pv.metallic === 'number' ? pv.metallic : 0;
1616
+ const roughnessPv = typeof pv.roughness === 'number' ? pv.roughness : 0.5;
1617
+ const clearcoatPv = typeof pv.clearcoat === 'number' ? pv.clearcoat : 0;
1618
+ const clearcoatRoughnessPv =
1619
+ typeof pv.clearcoatRoughness === 'number' ? pv.clearcoatRoughness : 0.5;
1620
+ const specularTintPv = pv.specularTint as readonly number[] | undefined;
1621
+ const normalScalePv = materialNormalScale(pv);
1622
+
1623
+ const paramSnap: Record<string, number | number[] | string> = {};
1624
+ for (const [k, v] of Object.entries(pv)) {
1625
+ if (typeof v === 'number') paramSnap[k] = v;
1626
+ else if (typeof v === 'string') paramSnap[k] = v;
1627
+ else if (Array.isArray(v) && v.every((x) => typeof x === 'number')) {
1628
+ paramSnap[k] = v as number[];
2227
1629
  }
2228
1630
  }
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
- }),
1631
+ const materialParamSchema = materialParametersToParamSchema(
1632
+ resolved.parameters ?? [],
1633
+ firstPassShader,
1634
+ );
1635
+ // feat-20260614 M8 (D-19): texture / sampler values are embedded GUIDs
1636
+ // (dash-form strings) after loadByGuid. Resolve each to a user-tier column
1637
+ // handle by looking up the catalogued payload and minting via
1638
+ // world.allocSharedRef; a numeric value (already a column handle from a
1639
+ // directly-minted material) passes through unchanged.
1640
+ const resolveTexLike = <B extends string>(
1641
+ value: unknown,
1642
+ brand: B,
1643
+ ): Handle<B, 'shared'> | undefined => {
1644
+ if (typeof value === 'number') return toShared<B>(value);
1645
+ if (typeof value === 'string') {
1646
+ // M4: intern the GUID -> column-handle resolution so each unique
1647
+ // (world, guid, brand) mints exactly ONE stable handle reused across
1648
+ // frames (stops the per-frame slot churn that defeated the GPU
1649
+ // residency cache). feat-20260619 M2 / w8: TextureAsset brand wires
1650
+ // onLastRelease -> gpuStore.evictTexture; other brands (SamplerAsset)
1651
+ // bypass (releaseUnreferenced-fallback lifecycle).
1652
+ return internSharedRefFromGuid(world, assetsRef, value, brand);
1653
+ }
1654
+ return undefined;
1655
+ };
1656
+ // feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): iterate the
1657
+ // shader's derive(paramSchema).textureFieldNames SSOT instead of a hardcoded
1658
+ // user-region field list, so an Nth texture (e.g. parallax heightTexture)
1659
+ // resolves through the same path. emissive/occlusion are engine-injection
1660
+ // (lightmap) textures, NOT in textureFieldNames, so they keep named reads.
1661
+ const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
1662
+ const textureHandles = collectUserRegionTextureHandles(
1663
+ pv,
1664
+ firstPassShader,
1665
+ materialParamSchema.length > 0 ? materialParamSchema : undefined,
1666
+ assetsRef,
1667
+ world,
1668
+ resolveTexLike,
1669
+ videoTextureFields,
1670
+ );
1671
+ const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
1672
+ resolveTexLike(materialTextureRef(value), 'SamplerAsset'),
1673
+ );
1674
+ const emissiveTextureHandle = resolveTexLike(
1675
+ materialTextureRef(pv.emissiveTexture),
1676
+ 'TextureAsset',
1677
+ );
1678
+ const occlusionTextureHandle = resolveTexLike(
1679
+ materialTextureRef(pv.occlusionTexture),
1680
+ 'TextureAsset',
1681
+ );
1682
+ const textureCoordinates = collectMaterialTextureCoordinates(pv);
1683
+ // Named user-region fields are derived from the map (sprite + legacy reads).
1684
+ const baseColorTextureHandle = textureHandles.get('baseColorTexture');
1685
+ const metallicRoughnessTextureHandle = textureHandles.get('metallicRoughnessTexture');
1686
+ const normalTextureHandle = textureHandles.get('normalTexture');
1687
+ const emissivePv = pv.emissive as readonly number[] | undefined;
1688
+ const snapshot: MaterialSnapshot = {
1689
+ baseColor,
1690
+ metallic: metallicPv,
1691
+ roughness: roughnessPv,
1692
+ clearcoat: clearcoatPv,
1693
+ clearcoatRoughness: clearcoatRoughnessPv,
1694
+ ...(specularTintPv !== undefined && {
1695
+ specularTint: [
1696
+ specularTintPv[0] ?? 1,
1697
+ specularTintPv[1] ?? 1,
1698
+ specularTintPv[2] ?? 1,
1699
+ ] as readonly [number, number, number],
1700
+ }),
1701
+ normalScale: normalScalePv,
1702
+ materialShaderId: firstPassShader,
1703
+ materialHandle: handleRaw,
1704
+ renderState: pipelineRenderState(allPasses[0]?.renderState),
1705
+ paramSnapshot: paramSnap,
1706
+ ...(materialParamSchema.length > 0 && { materialParamSchema }),
1707
+ ...(textureCoordinates.size > 0 && { textureCoordinates }),
1708
+ ...(textureHandles.size > 0 && { textureHandles }),
1709
+ ...(videoTextureFields.size > 0 && { videoTextureFields }),
1710
+ ...(samplerHandles.size > 0 && { samplerHandles }),
1711
+ ...(baseColorTextureHandle !== undefined && { baseColorTexture: baseColorTextureHandle }),
1712
+ ...(metallicRoughnessTextureHandle !== undefined && {
1713
+ metallicRoughnessTexture: metallicRoughnessTextureHandle,
1714
+ }),
1715
+ ...(normalTextureHandle !== undefined && { normalTexture: normalTextureHandle }),
1716
+ ...(emissivePv !== undefined && {
1717
+ emissive: [emissivePv[0] ?? 0, emissivePv[1] ?? 0, emissivePv[2] ?? 0] as readonly [
1718
+ number,
1719
+ number,
1720
+ number,
1721
+ ],
1722
+ }),
1723
+ ...(typeof pv.emissiveIntensity === 'number' && { emissiveIntensity: pv.emissiveIntensity }),
1724
+ ...(emissiveTextureHandle !== undefined && { emissiveTexture: emissiveTextureHandle }),
1725
+ ...(occlusionTextureHandle !== undefined && { occlusionTexture: occlusionTextureHandle }),
1726
+ ...(typeof pv.occlusionStrength === 'number' && { occlusionStrength: pv.occlusionStrength }),
1727
+ // feat-city-glb Bug 5 (per-submesh transparency): derive `transparent`
1728
+ // from the first pass's `renderState.blend` presence, identical to the
1729
+ // entity-level snapshot builder (extractFrame archetype loop). Without
1730
+ // this, per-submesh materials (materials[i>=1], e.g. a glTF BLEND decal
1731
+ // submesh on a multi-material mesh) never carry the transparent flag, so
1732
+ // the record stage's LDR split + blend routing treats them as opaque and
1733
+ // their alpha=0 texels composite as black.
1734
+ transparent: allPasses[0]?.renderState?.blend !== undefined,
2256
1735
  };
1736
+ const stored =
1737
+ materialSnapshotCache !== undefined
1738
+ ? storeMaterialSnapshot(
1739
+ materialSnapshotCache,
1740
+ handleRaw,
1741
+ snapshot,
1742
+ allPasses,
1743
+ asset,
1744
+ assetsRef,
1745
+ )
1746
+ : undefined;
1747
+ const cacheEntry =
1748
+ stored ??
1749
+ (persistentMaterialSnapshotCache !== undefined
1750
+ ? storeMaterialSnapshot(
1751
+ persistentMaterialSnapshotCache,
1752
+ handleRaw,
1753
+ snapshot,
1754
+ allPasses,
1755
+ asset,
1756
+ assetsRef,
1757
+ )
1758
+ : undefined);
1759
+ if (cacheEntry?.crossFrameSafe === true) {
1760
+ persistentMaterialSnapshotCache?.set(handleRaw, cacheEntry);
1761
+ }
1762
+ return snapshot;
2257
1763
  }
2258
1764
 
2259
1765
  /**
@@ -2269,6 +1775,132 @@ export interface ExtractPipelineSurface {
2269
1775
  readonly skinPaletteAllocator: SkinPaletteAllocator | null;
2270
1776
  }
2271
1777
 
1778
+ /**
1779
+ * PSSM (Parallel-Split Shadow Maps) split plane computation.
1780
+ *
1781
+ * Formula (plan-strategy D-8, research F1):
1782
+ * C_i = λ·n·(f/n)^(i/m) + (1-λ)·(n + i/m·(f-n))
1783
+ * where i = 1..m, n = near, f = far, m = cascadeCount
1784
+ *
1785
+ * The coverage range is [camera near, DirectionalLight.shadowDistance] — the
1786
+ * near end derives from the active camera, the far end is the shadowDistance
1787
+ * knob. Returns strictly monotonic view-space z depths (positive).
1788
+ *
1789
+ * When `far <= near + ε` (ε=1e-6), throws ShadowInvalidConfigError
1790
+ * (charter P3 explicit failure — research F2 guarantees formula stability
1791
+ * but degenerate configuration must be surfaced to the caller).
1792
+ *
1793
+ * @param nearPlane - near of the shadow casting range (from the camera near)
1794
+ * @param farPlane - far of the shadow casting range (DirectionalLight.shadowDistance)
1795
+ * @param cascadeCount - number of cascades (1..4)
1796
+ * @param splitLambda - PSSM split weight (0 = pure uniform, 1 = pure log)
1797
+ * @returns Array of m split depths (view-space z); length = cascadeCount,
1798
+ * guaranteed strictly monotonic, last element = far.
1799
+ *
1800
+ * @internal exported for w6/w7 testing only; production callers route through
1801
+ * extractFrame which validates cascadeCount/splitLambda via component schema.
1802
+ */
1803
+ /**
1804
+ * Compute the 8 world-space corner points of a camera frustum slice.
1805
+ *
1806
+ * Uses mat4.unproject to map NDC cube corners back to world space.
1807
+ * The slice is defined by view-space nearZ and farZ depths.
1808
+ *
1809
+ * For a WebGPU perspective matrix, ndcZ(viewZ) = camFar * (viewZ - camNear) /
1810
+ * (viewZ * (camFar - camNear)). We compute the ndcZ for each slice boundary
1811
+ * and unproject directly.
1812
+ *
1813
+ * @param vp - camera view-projection matrix (column-major 16 floats)
1814
+ * @param camNear - camera near plane (used to derive NDC mapping)
1815
+ * @param camFar - camera far plane (used to derive NDC mapping)
1816
+ * @param nearZ - near depth of the frustum slice (view-space, positive)
1817
+ * @param farZ - far depth of the frustum slice (view-space, positive)
1818
+ * @returns Array of 8 Vec3 world-space corner positions.
1819
+ */
1820
+ function computeFrustumCorners(
1821
+ vp: Mat4,
1822
+ camNear: number,
1823
+ camFar: number,
1824
+ nearZ: number,
1825
+ farZ: number,
1826
+ projection: 'perspective' | 'orthographic',
1827
+ ): Vec3[] {
1828
+ const invVP = mat4.create();
1829
+ mat4.invert(invVP, vp);
1830
+ const corners: Vec3[] = [];
1831
+
1832
+ // NDC z mapping is projection-dependent (WebGPU clip-space z in [0,1]):
1833
+ // perspective: ndc(z) = camFar * (z - camNear) / (z * (camFar - camNear))
1834
+ // orthographic: ndc(z) = (z - camNear) / (camFar - camNear)
1835
+ // feat-20260613-csm M6 / w22: orthographic cameras silently produced
1836
+ // garbage NDC z (perspective formula divides by viewZ, but ortho NDC is
1837
+ // linear), which mapped the cascade slab back to a degenerate world-space
1838
+ // corner set and the AABB-fit collapsed to near zero -- shadow_caster
1839
+ // wrote its triangles outside the [-1,1] clip volume so the depth
1840
+ // attachment stayed at clear=1.0 (root cause for shadow-m2 / shadow-m3 /
1841
+ // shadow-opt-out dawn red surfaced after w20's host-side ortho fix).
1842
+ const span = camFar - camNear;
1843
+ const ndcNear =
1844
+ projection === 'orthographic'
1845
+ ? (nearZ - camNear) / span
1846
+ : (camFar * (nearZ - camNear)) / (nearZ * span);
1847
+ const ndcFar =
1848
+ projection === 'orthographic'
1849
+ ? (farZ - camNear) / span
1850
+ : (camFar * (farZ - camNear)) / (farZ * span);
1851
+
1852
+ const signs = [-1, 1];
1853
+ for (const sx of signs) {
1854
+ for (const sy of signs) {
1855
+ corners.push(unprojectNDC(invVP, sx, sy, ndcNear));
1856
+ }
1857
+ }
1858
+ for (const sx of signs) {
1859
+ for (const sy of signs) {
1860
+ corners.push(unprojectNDC(invVP, sx, sy, ndcFar));
1861
+ }
1862
+ }
1863
+
1864
+ return corners;
1865
+ }
1866
+
1867
+ /**
1868
+ * Unproject a single NDC point to world space.
1869
+ */
1870
+ function unprojectNDC(invVP: Mat4, ndcX: number, ndcY: number, ndcZ: number): Vec3 {
1871
+ const ndc = vec3.create(ndcX, ndcY, ndcZ);
1872
+ const ws = vec3.create();
1873
+ mat4.unproject(ws, ndc, invVP);
1874
+ return ws;
1875
+ }
1876
+
1877
+ export function pssmSplit(
1878
+ nearPlane: number,
1879
+ farPlane: number,
1880
+ cascadeCount: number,
1881
+ splitLambda: number,
1882
+ ): Float32Array {
1883
+ const EPS = 1e-6;
1884
+ if (farPlane <= nearPlane + EPS) {
1885
+ throw new ShadowInvalidConfigError('shadowDistance', farPlane, nearPlane + EPS);
1886
+ }
1887
+
1888
+ const result = new Float32Array(cascadeCount);
1889
+ const m = cascadeCount;
1890
+ const n = nearPlane;
1891
+ const f = farPlane;
1892
+ const ratio = f / n;
1893
+
1894
+ for (let i = 1; i <= m; i++) {
1895
+ const t = i / m;
1896
+ const logPart = n * ratio ** t;
1897
+ const uniformPart = n + t * (f - n);
1898
+ result[i - 1] = splitLambda * logPart + (1 - splitLambda) * uniformPart;
1899
+ }
1900
+
1901
+ return result;
1902
+ }
1903
+
2272
1904
  // feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-5 (plan-strategy §D-3,
2273
1905
  // requirements §5.3): 6-face view-proj matrix table for one omnidirectional
2274
1906
  // shadow caster.
@@ -2342,8 +1974,196 @@ export function buildPointShadowMatrices(lightPos: Vec3, near: number, far: numb
2342
1974
  return out;
2343
1975
  }
2344
1976
 
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.
1977
+ /**
1978
+ * bug-20260710-editor-cross-world-shadow: the camera-frustum subset of
1979
+ * {@link CameraSnapshot} that the directional-CSM matrix builder consumes.
1980
+ * Extracted so the builder can run both inside {@link extractFrame} (single
1981
+ * world) AND at the {@link extractFrames} merge layer, where the camera and the
1982
+ * directional light may originate in DIFFERENT worlds (the editor
1983
+ * editorWorld/sceneWorld super-composite). A cameraless world produces zero
1984
+ * lightViewProj matrices, so the merge layer MUST recompute using the surfaced
1985
+ * camera — see {@link computeDirectionalCsm}.
1986
+ */
1987
+ export interface CsmCameraData {
1988
+ readonly world: Float32Array;
1989
+ readonly fov: number;
1990
+ readonly aspect: number;
1991
+ readonly near: number;
1992
+ readonly far: number;
1993
+ readonly projection: 'perspective' | 'orthographic';
1994
+ readonly orthoLeft: number;
1995
+ readonly orthoRight: number;
1996
+ readonly orthoBottom: number;
1997
+ readonly orthoTop: number;
1998
+ }
1999
+
2000
+ /**
2001
+ * bug-20260710-editor-cross-world-shadow: the raw directional-shadow config a
2002
+ * DirectionalLight carries, threaded onto {@link ExtractedLights} so the merge
2003
+ * layer can recompute CSM matrices against the surfaced camera. Distinct from
2004
+ * the *computed* outputs (lightViewProj / splitPlanes) also on ExtractedLights.
2005
+ */
2006
+ export interface DirectionalCsmConfig {
2007
+ readonly cascadeCount: number;
2008
+ readonly splitLambda: number;
2009
+ readonly cascadeBlend: number;
2010
+ readonly mapSize: number;
2011
+ readonly shadowDistance: number;
2012
+ }
2013
+
2014
+ /**
2015
+ * bug-20260710-editor-cross-world-shadow: computed directional-CSM output —
2016
+ * the per-cascade light-view-projection matrices plus the derived cascade
2017
+ * metadata. Returned by {@link computeDirectionalCsm}; `undefined` when the
2018
+ * config or camera is missing (caller leaves the fields undefined).
2019
+ */
2020
+ export interface DirectionalCsmResult {
2021
+ readonly lightViewProj: Float32Array[];
2022
+ readonly splitPlanes: Float32Array;
2023
+ readonly cascadeCount: number;
2024
+ readonly cascadeBlend: number;
2025
+ readonly shadowMapSize: number;
2026
+ }
2027
+
2028
+ /**
2029
+ * bug-20260710-editor-cross-world-shadow: pure directional-CSM matrix builder,
2030
+ * extracted verbatim from the former inline block in {@link extractFrame} (the
2031
+ * PSSM split + per-cascade frustum-slice AABB fit + orthographic light
2032
+ * projection). It is now a free function so BOTH the per-world extract and the
2033
+ * cross-world merge layer can call it — the merge layer is the only place that
2034
+ * pairs the first-hit directional light of one world with the surfaced camera
2035
+ * of another (editor super-composite). Returns `null` when there is no camera
2036
+ * to fit against (a cameraless world's directional light yields no matrices —
2037
+ * the caller leaves lightViewProj undefined rather than emitting zero matrices
2038
+ * that the WGSL reader would sample as NaN → "fully lit").
2039
+ *
2040
+ * Byte-identical to the prior inline computation for the single-world case
2041
+ * (same PSSM lambda, same toward-light Z reach RC-2 fix, same clip-space
2042
+ * matrix / tile-placement-in-shader split).
2043
+ */
2044
+ export function computeDirectionalCsm(
2045
+ direction: Vec3,
2046
+ config: DirectionalCsmConfig,
2047
+ cameraData: CsmCameraData | undefined,
2048
+ ): DirectionalCsmResult | null {
2049
+ const cascadeCount = Math.round(config.cascadeCount);
2050
+ // Coverage range: near from the active camera near (no separate near knob);
2051
+ // far is the component's shadowDistance. Fallback near (0.1) only when no
2052
+ // camera exists — but matrices are gated on cameraData below anyway.
2053
+ const sNear = cameraData?.near ?? 0.1;
2054
+ const sFar = config.shadowDistance;
2055
+
2056
+ // PSSM split planes: [camera near, shadowDistance], not the camera far.
2057
+ const splits = pssmSplit(sNear, sFar, cascadeCount, config.splitLambda);
2058
+ const paddedSplits = new Float32Array(4);
2059
+ for (let i = 0; i < splits.length; i++) paddedSplits[i] = splits[i] ?? 0;
2060
+
2061
+ // Light view matrix: camera at origin, looking along the light direction.
2062
+ const lightDirN = vec3.normalize(vec3.create(), direction);
2063
+ const lightTarget = vec3.create(lightDirN[0] ?? 0, lightDirN[1] ?? 0, lightDirN[2] ?? 0);
2064
+ const lightView = mat4.create();
2065
+ mat4.lookAt(lightView, vec3.create(0, 0, 0), lightTarget, vec3.create(0, 1, 0));
2066
+
2067
+ // Camera view-projection for frustum-corner computation. Without a camera the
2068
+ // frustum cannot be fit — bail so the caller emits no matrices (undefined),
2069
+ // NOT zero matrices (which read as NaN in the shadow shader → no shadow).
2070
+ if (cameraData === undefined) return null;
2071
+ const camProj = mat4.create();
2072
+ if (cameraData.projection === 'orthographic') {
2073
+ mat4.orthographic(
2074
+ camProj,
2075
+ cameraData.orthoLeft,
2076
+ cameraData.orthoRight,
2077
+ cameraData.orthoBottom,
2078
+ cameraData.orthoTop,
2079
+ cameraData.near,
2080
+ cameraData.far,
2081
+ );
2082
+ } else {
2083
+ mat4.perspective(camProj, cameraData.fov, cameraData.aspect, cameraData.near, cameraData.far);
2084
+ }
2085
+ const camView = mat4.create();
2086
+ mat4.invert(camView, cameraData.world);
2087
+ const cameraVP = mat4.create();
2088
+ mat4.multiply(cameraVP, camProj, camView);
2089
+
2090
+ const resultLightViewProjs: Float32Array[] = [];
2091
+
2092
+ // bug-20260619 RC-2 (AC-05): toward-light Z reach. Extend the near (toward-
2093
+ // light) bound of EVERY cascade to the toward-light extreme of the WHOLE
2094
+ // shadow frustum (sNear..sFar) so casters between the light and a slice are
2095
+ // admitted; X/Y stays per-cascade tight. Larger light-space z == closer to
2096
+ // the light (lookAt forward = eye-target), so the full-frustum max-z is the
2097
+ // toward-light reach used as -maxZ (the ortho near plane) per cascade.
2098
+ let lightSpaceMaxZFull = -Infinity;
2099
+ const fullCorners = computeFrustumCorners(
2100
+ cameraVP,
2101
+ cameraData.near,
2102
+ cameraData.far,
2103
+ sNear,
2104
+ sFar,
2105
+ cameraData.projection,
2106
+ );
2107
+ for (const ws of fullCorners) {
2108
+ const ls = vec3.create();
2109
+ mat4.transformVec3(ls, lightView, ws);
2110
+ if ((ls[2] ?? 0) > lightSpaceMaxZFull) lightSpaceMaxZFull = ls[2] ?? 0;
2111
+ }
2112
+
2113
+ // Pre-allocate 4-cascade array; fill in the effective cascades.
2114
+ for (let cIdx = 0; cIdx < 4; cIdx++) {
2115
+ if (cIdx >= cascadeCount) {
2116
+ resultLightViewProjs.push(new Float32Array(16));
2117
+ continue;
2118
+ }
2119
+ const cascadeNear = cIdx === 0 ? sNear : (splits[cIdx - 1] ?? sFar);
2120
+ const cascadeFar = splits[cIdx] ?? sFar;
2121
+ const corners = computeFrustumCorners(
2122
+ cameraVP,
2123
+ cameraData.near,
2124
+ cameraData.far,
2125
+ cascadeNear,
2126
+ cascadeFar,
2127
+ cameraData.projection,
2128
+ );
2129
+ const lightMVP = mat4.clone(lightView);
2130
+ let minX = Infinity;
2131
+ let maxX = -Infinity;
2132
+ let minY = Infinity;
2133
+ let maxY = -Infinity;
2134
+ let minZ = Infinity;
2135
+ let maxZ = -Infinity;
2136
+ for (const ws of corners) {
2137
+ const ls = vec3.create();
2138
+ mat4.transformVec3(ls, lightMVP, ws);
2139
+ if ((ls[0] ?? 0) < minX) minX = ls[0] ?? 0;
2140
+ if ((ls[0] ?? 0) > maxX) maxX = ls[0] ?? 0;
2141
+ if ((ls[1] ?? 0) < minY) minY = ls[1] ?? 0;
2142
+ if ((ls[1] ?? 0) > maxY) maxY = ls[1] ?? 0;
2143
+ if ((ls[2] ?? 0) < minZ) minZ = ls[2] ?? 0;
2144
+ if ((ls[2] ?? 0) > maxZ) maxZ = ls[2] ?? 0;
2145
+ }
2146
+ // Orthographic projection from light-space AABB. RC-2: whole-frustum
2147
+ // toward-light extreme for the near bound; per-cascade far (minZ) + X/Y.
2148
+ const nearZ = Math.max(maxZ, lightSpaceMaxZFull);
2149
+ const orthoProj = mat4.create();
2150
+ mat4.orthographic(orthoProj, minX, maxX, minY, maxY, -nearZ, -minZ);
2151
+ // lightViewProj = orthoProj * lightView — pure clip-space [-1,1]. Atlas
2152
+ // tile placement is handled by the per-cascade viewport + fragment-side UV
2153
+ // math (evalDirectional), so shadow_caster.gl_Position stays clip-space.
2154
+ void cIdx;
2155
+ resultLightViewProjs.push(new Float32Array(mat4.multiply(mat4.create(), orthoProj, lightView)));
2156
+ }
2157
+
2158
+ return {
2159
+ lightViewProj: resultLightViewProjs,
2160
+ splitPlanes: paddedSplits,
2161
+ cascadeCount,
2162
+ cascadeBlend: config.cascadeBlend,
2163
+ shadowMapSize: config.mapSize,
2164
+ };
2165
+ }
2166
+
2347
2167
  /**
2348
2168
  * feat-20260709-editor-world-partition M1 / w4 (AC-08, plan-strategy §2 D-3):
2349
2169
  * the owner index that previously served BOTH cameras and singleton render
@@ -2360,42 +2180,29 @@ export interface ExtractFramesOwner {
2360
2180
  }
2361
2181
 
2362
2182
  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;
2183
+ readonly assets: AssetRegistry | null | undefined;
2366
2184
  readonly pipelineState: ExtractPipelineSurface | null | undefined;
2367
- readonly gpuStore: import('./gpu-resource-store').GpuResourceStore | undefined;
2368
2185
  readonly materialSnapshotCache: MaterialSnapshotCache | undefined;
2369
2186
  readonly cull: 'self' | 'none' | 'external';
2370
2187
  readonly cullCameras: readonly CameraSnapshot[] | undefined;
2371
2188
  readonly hierarchy: SceneHierarchySnapshot;
2372
2189
  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[];
2383
2190
  }
2384
2191
 
2385
2192
  export function extractCameraSnapshots(world: World): CameraSnapshot[] {
2193
+ const worldInternal = createWorldInternalView(world);
2386
2194
  const cameras: CameraSnapshot[] = [];
2387
2195
  const cameraEntities: number[] = [];
2388
- const cameraQuery = world.query({ read: [Camera, Transform] }).unwrap();
2196
+ const cameraQuery = world.query({ read: [Camera], with: [Transform] }).unwrap();
2389
2197
  for (const row of cameraQuery) {
2390
2198
  const cam = row.get(Camera);
2391
2199
  const entity = row.entity;
2392
- const transform = row.get(Transform);
2393
- if (transform === undefined) continue;
2394
- const worldMat = new Float32Array(transform.world);
2200
+ const view = worldInternal._getArrayView(entity, Transform, 'world');
2201
+ if (view === undefined) continue;
2202
+ const worldMat = new Float32Array(view);
2395
2203
  cameras.push({
2396
- worldId: 0,
2397
2204
  entityKey: entity as number,
2398
- position: mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like),
2205
+ position: mat4.getTranslation(vec3.create(), worldMat),
2399
2206
  world: worldMat,
2400
2207
  fov: cam.fov,
2401
2208
  aspect: cam.aspect,
@@ -2410,7 +2217,6 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
2410
2217
  exposure: cam.exposure,
2411
2218
  whitePoint: cam.whitePoint,
2412
2219
  antialias: antialiasFromF32(cam.antialias),
2413
- historyVersion: cam.historyVersion,
2414
2220
  bloom: bloomEnabledFromF32(cam.bloom),
2415
2221
  bloomThreshold: cam.bloomThreshold,
2416
2222
  bloomIntensity: cam.bloomIntensity,
@@ -2440,114 +2246,35 @@ function tonemapParams(camera: CameraSnapshot): Uint8Array {
2440
2246
  return new Uint8Array(bytes);
2441
2247
  }
2442
2248
 
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
2249
  export function prepareExtractContext(
2511
2250
  world: World,
2512
2251
  options: {
2513
- readonly worldId?: number;
2514
- readonly assets?: RenderAssetPort | null;
2252
+ readonly assets?: AssetRegistry | null;
2515
2253
  readonly pipelineState?: ExtractPipelineSurface | null;
2516
- readonly gpuStore?: import('./gpu-resource-store').GpuResourceStore;
2517
2254
  readonly materialSnapshotCache?: MaterialSnapshotCache;
2518
2255
  readonly cull?: 'self' | 'none' | 'external';
2519
2256
  readonly cullCameras?: readonly CameraSnapshot[];
2520
- readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus;
2521
2257
  } = {},
2522
2258
  ): PreparedExtractContext {
2523
2259
  const hierarchy = projectHierarchy(world);
2524
2260
  return {
2525
- worldId: options.worldId ?? 0,
2526
2261
  assets: options.assets,
2527
2262
  pipelineState: options.pipelineState,
2528
- gpuStore: options.gpuStore,
2529
2263
  materialSnapshotCache: options.materialSnapshotCache,
2530
2264
  cull: options.cull ?? 'self',
2531
2265
  cullCameras: options.cullCameras,
2532
2266
  hierarchy,
2533
2267
  visibility: resolveVisibility(world, hierarchy),
2534
- ...(options.paramSchemaProjectionCensus === undefined
2535
- ? {}
2536
- : { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
2537
2268
  };
2538
2269
  }
2539
2270
 
2540
2271
  export function extractFrames(
2541
2272
  worlds: readonly World[],
2542
2273
  owner: number | ExtractFramesOwner,
2543
- assets?: RenderAssetPort | null,
2274
+ assets?: AssetRegistry | null,
2544
2275
  pipelineState?: ExtractPipelineSurface | null,
2545
- gpuStore?: import('./gpu-resource-store').GpuResourceStore,
2546
2276
  materialSnapshotCachesByWorld?: MaterialSnapshotCachesByWorld,
2547
- options: {
2548
- readonly cull?: 'normal' | 'none';
2549
- readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus;
2550
- } = {},
2277
+ options: { readonly cull?: 'normal' | 'none' } = {},
2551
2278
  ): ExtractedFrame {
2552
2279
  // w4: normalize the owner argument. A bare number is the legacy single-owner
2553
2280
  // form (cameraOwner === resourceOwner); an object carries the two split
@@ -2559,7 +2286,7 @@ export function extractFrames(
2559
2286
  // ── D-2: frame-level side effects live here ────────────────────────────
2560
2287
  //
2561
2288
  // resetForFrame is called exactly once per frame, at the extractFrames
2562
- // entry. The skinPaletteAllocator cursor is reset before any per-world
2289
+ // entry. The skinPaletteAllocator cursor is reset before each per-world
2563
2290
  // extract runs, so sequential per-world allocation yields non-overlapping
2564
2291
  // palette slices (AC-08).
2565
2292
  const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
@@ -2567,55 +2294,7 @@ export function extractFrames(
2567
2294
  skinPaletteAllocator.resetForFrame();
2568
2295
  }
2569
2296
 
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
2297
  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
2298
  // ── D-2: per-world extract with error isolation ────────────────────────
2620
2299
  //
2621
2300
  // Each world runs extractFrame over the final state published by
@@ -2644,10 +2323,8 @@ export function extractFrames(
2644
2323
  const isCameraOwner = wi === cameraOwner;
2645
2324
  const cameraOwnerFrame = framesByWorld.get(cameraOwner);
2646
2325
  const prepared = prepareExtractContext(world, {
2647
- worldId: wi,
2648
2326
  ...(assets !== undefined ? { assets } : {}),
2649
2327
  ...(pipelineState !== undefined ? { pipelineState } : {}),
2650
- ...(gpuStore !== undefined ? { gpuStore } : {}),
2651
2328
  ...(materialSnapshotCachesByWorld === undefined
2652
2329
  ? {}
2653
2330
  : {
@@ -2661,9 +2338,6 @@ export function extractFrames(
2661
2338
  }),
2662
2339
  cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
2663
2340
  ...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
2664
- ...(options.paramSchemaProjectionCensus === undefined
2665
- ? {}
2666
- : { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
2667
2341
  });
2668
2342
  const frame = extractFrame(world, prepared);
2669
2343
 
@@ -2671,7 +2345,10 @@ export function extractFrames(
2671
2345
  } catch (err) {
2672
2346
  // Per-world failure: route to world's own error handler, skip contribution.
2673
2347
  try {
2674
- reportRenderError(err);
2348
+ createWorldInternalView(world)._routeError(err, {
2349
+ severity: Severity.Error,
2350
+ systemName: `RenderSystem.extractFrames(world[${wi}])`,
2351
+ });
2675
2352
  } catch {
2676
2353
  // If _routeError itself throws, the world already failed — skip silently.
2677
2354
  }
@@ -2690,9 +2367,7 @@ export function extractFrames(
2690
2367
 
2691
2368
  // AC-04: renderables — concat by worlds[] order, stamp worldId.
2692
2369
  const renderables: RenderableSnapshot[] = [];
2693
- const candidateRenderables: RenderableSnapshot[] = [];
2694
2370
  const dispatchEntries: DispatchEntry[] = [];
2695
- const candidateDispatchEntries: DispatchEntry[] = [];
2696
2371
  const visibilitySnapshots: VisibilitySnapshot[] = [];
2697
2372
  const featureVisibilitySnapshots: RenderFeatureWorldVisibilitySnapshot[] = [];
2698
2373
  const hiddenEntityReports: RenderFeatureHiddenEntityReport[] = [];
@@ -2703,7 +2378,6 @@ export function extractFrames(
2703
2378
  if (f === undefined || wId === undefined) continue;
2704
2379
 
2705
2380
  const base = renderables.length;
2706
- const candidateBase = candidateRenderables.length;
2707
2381
  const visibilitySnapshot = f.visibilitySnapshots[0];
2708
2382
  const world = worlds[wId];
2709
2383
  if (visibilitySnapshot !== undefined) {
@@ -2713,44 +2387,17 @@ export function extractFrames(
2713
2387
  }
2714
2388
  hiddenEntityReports.push(...f.hiddenEntityReports);
2715
2389
  for (const r of f.renderables) {
2716
- renderables.push({
2717
- ...r,
2718
- 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
- : {}),
2731
- });
2390
+ renderables.push({ ...r, worldId: wId });
2732
2391
  }
2733
2392
 
2734
2393
  // D-3: dispatch — per-world renderableIndex rebased by base offset.
2735
2394
  for (const d of f.dispatch) {
2736
2395
  dispatchEntries.push({ ...d, renderableIndex: (d.renderableIndex ?? 0) + base });
2737
2396
  }
2738
- for (const d of f.candidateDispatch ?? f.dispatch) {
2739
- candidateDispatchEntries.push({
2740
- ...d,
2741
- renderableIndex: (d.renderableIndex ?? 0) + candidateBase,
2742
- });
2743
- }
2744
2397
  }
2745
2398
 
2746
2399
  // Stable sort dispatch by queue value.
2747
2400
  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
2401
 
2755
2402
  // AC-04: lights — point[]/spot[] concat; directional first-hit in
2756
2403
  // succeededFrames order (which preserves worlds[] order for successful
@@ -2767,7 +2414,6 @@ export function extractFrames(
2767
2414
  let depthBias: number | undefined;
2768
2415
  let normalBias: number | undefined;
2769
2416
  let pcfKernelSize: number | undefined;
2770
- let directionalCsm: DirectionalCsmResult | undefined;
2771
2417
  const pointShadow: PointShadowSnapshot[] = [];
2772
2418
  // bug-20260710-editor-cross-world-shadow: carry the raw CSM config +
2773
2419
  // direction of the first-hit directional so the merge layer can recompute
@@ -2807,10 +2453,7 @@ export function extractFrames(
2807
2453
  if (succeededIndices[fi] === cameraOwner) cameraOwnerFrame = succeededFrames[fi];
2808
2454
  if (succeededIndices[fi] === resourceOwner) resourceOwnerFrame = succeededFrames[fi];
2809
2455
  }
2810
- const cameras =
2811
- cameraOwnerFrame !== undefined
2812
- ? cameraOwnerFrame.cameras.map((camera) => ({ ...camera, worldId: cameraOwner }))
2813
- : [];
2456
+ const cameras = cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.cameras] : [];
2814
2457
 
2815
2458
  // bug-20260710-editor-cross-world-shadow: RECOMPUTE directional CSM matrices
2816
2459
  // against the surfaced camera. In a single-world app the per-world extract
@@ -2842,10 +2485,8 @@ export function extractFrames(
2842
2485
  directionalCsmDirection,
2843
2486
  directionalCsmConfig,
2844
2487
  mergeCameraData,
2845
- renderCandidateCsmCandidates(candidateDomain),
2846
2488
  );
2847
2489
  if (csm !== null) {
2848
- directionalCsm = csm;
2849
2490
  lightViewProj = csm.lightViewProj;
2850
2491
  splitPlanes = csm.splitPlanes;
2851
2492
  cascadeCount = csm.cascadeCount;
@@ -2872,35 +2513,30 @@ export function extractFrames(
2872
2513
  pointShadow,
2873
2514
  directionalCsmConfig,
2874
2515
  directionalCsmDirection,
2875
- ...(directionalCsm !== undefined ? { directionalCsm, cascades: directionalCsm.cascades } : {}),
2876
2516
  };
2877
- const environment =
2878
- resourceOwnerFrame?.environment ??
2879
- ({
2880
- kind: 'none',
2881
- worldId: resourceOwner,
2882
- skylightCount: 0,
2883
- skyboxCount: 0,
2884
- } satisfies ExtractedEnvironment);
2517
+ const skylight = resourceOwnerFrame?.skylight;
2518
+ const skylightCount = resourceOwnerFrame?.skylightCount ?? 0;
2519
+ const skybox = resourceOwnerFrame?.skybox;
2520
+ const skyboxCount = resourceOwnerFrame?.skyboxCount ?? 0;
2885
2521
  // User-authored PostProcessParams entities are singleton scene resources, so
2886
2522
  // they come from the resourceOwner world (holistic snapshot selection).
2887
2523
  const postProcessParams = new Map(resourceOwnerFrame?.postProcessParams);
2888
2524
  // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 1): the
2889
- // engine built-in `'forgeax::tonemap'` param is NOT a scene resource — the
2525
+ // engine built-in Standard tonemap param is NOT a scene resource — the
2890
2526
  // per-world extractFrame bridges it from that world's own `cameras[0]`
2891
2527
  // (Camera.exposure / whitePoint / tonemap is the SSOT). It therefore lives on
2892
2528
  // the CAMERA-owner frame, not the resource-owner frame. In the split-owner
2893
2529
  // editor topology the resourceOwner world has no Camera, so its frame carries
2894
- // no `forgeax::tonemap` entry; taking postProcessParams from resourceOwner
2530
+ // no Standard tonemap entry; taking postProcessParams from resourceOwner
2895
2531
  // alone drops it, the tonemap pass's params UBO stays zero-filled
2896
2532
  // (exposure=0 => tonemapped output is uniformly black), and the whole frame
2897
2533
  // reads black even though geometry drew into the HDR target. Overlay the
2898
2534
  // camera-owner frame's tonemap param (its SSOT source) so the surfaced
2899
2535
  // camera's exposure/whitePoint/mode reach the tonemap pass. When
2900
2536
  // 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';
2537
+ // Standard tonemap identity mirrors the engine provider key set at the
2538
+ // bottom of extractFrame (SSOT: same identity, same 16B layout).
2539
+ const TONEMAP_PARAM_KEY = STANDARD_TONEMAP_FEATURE_ID;
2904
2540
  const cameraTonemapParam = cameraOwnerFrame?.postProcessParams.get(TONEMAP_PARAM_KEY);
2905
2541
  if (cameraTonemapParam !== undefined) {
2906
2542
  postProcessParams.set(TONEMAP_PARAM_KEY, cameraTonemapParam);
@@ -2920,21 +2556,16 @@ export function extractFrames(
2920
2556
  const visibilityStats = {
2921
2557
  explicitlyHidden: succeededFrames.reduce((s, f) => s + f.visibilityStats.explicitlyHidden, 0),
2922
2558
  };
2923
- const resourceWorld = worlds[resourceOwner];
2924
- const fog = selectFog(resourceWorld, resourceOwner);
2925
- if (!fog.ok) routeFogSelectionError(resourceWorld, fog.error);
2926
2559
 
2927
2560
  return {
2928
2561
  cameras,
2929
2562
  lights,
2930
2563
  renderables,
2931
2564
  dispatch: dispatchEntries,
2932
- candidateRenderables,
2933
- candidateDispatch: candidateDispatchEntries,
2934
- candidateDispatchMembership,
2935
- candidateDomain,
2936
- fog: fog.ok ? fog.value.snapshot : undefined,
2937
- environment,
2565
+ skylight,
2566
+ skylightCount,
2567
+ skybox,
2568
+ skyboxCount,
2938
2569
  frustumStats,
2939
2570
  visibilityStats,
2940
2571
  postProcessParams,
@@ -2982,6 +2613,36 @@ function hasFiniteOrderedLocalAabb(aabb: Float32Array | undefined): aabb is Floa
2982
2613
  return minX <= maxX && minY <= maxY && minZ <= maxZ;
2983
2614
  }
2984
2615
 
2616
+ function morphSnapshotFor(
2617
+ mesh: MeshAsset,
2618
+ weights: ArrayLike<number> | undefined,
2619
+ ): MorphSnapshot | undefined {
2620
+ const targets = mesh.morphTargets;
2621
+ if (targets === undefined || targets.length === 0 || weights === undefined) return undefined;
2622
+ if (weights.length !== targets.length) return undefined;
2623
+ const firstPositions = targets[0]?.position;
2624
+ if (
2625
+ firstPositions === undefined ||
2626
+ firstPositions.length === 0 ||
2627
+ firstPositions.length % 3 !== 0
2628
+ ) {
2629
+ return undefined;
2630
+ }
2631
+ const vertexCount = firstPositions.length / 3;
2632
+ if (mesh.vertices.length % vertexCount !== 0) return undefined;
2633
+ for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
2634
+ const target = targets[targetIndex];
2635
+ if (target?.position?.length !== firstPositions.length) return undefined;
2636
+ }
2637
+ const copied = new Float32Array(weights.length);
2638
+ for (let index = 0; index < weights.length; index += 1) {
2639
+ const weight = weights[index] ?? Number.NaN;
2640
+ if (!Number.isFinite(weight)) return undefined;
2641
+ copied[index] = weight;
2642
+ }
2643
+ return { weights: copied, targetCount: targets.length };
2644
+ }
2645
+
2985
2646
  export function extractFrame(world: World, context: PreparedExtractContext): ExtractedFrame {
2986
2647
  // feat-20260708-composited-multi-world-rendering M2 / D-2: resetForFrame
2987
2648
  // has been lifted to extractFrames (the frame-level entry point).
@@ -2990,16 +2651,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2990
2651
  const {
2991
2652
  assets,
2992
2653
  pipelineState,
2993
- gpuStore,
2994
2654
  materialSnapshotCache: persistentMaterialSnapshotCache,
2995
2655
  cull: cullMode,
2996
2656
  } = context;
2997
- const reportRenderError = (
2998
- error: unknown,
2999
- errorContext?: { readonly systemName: string },
3000
- ): void => {
3001
- routeWorldError(world, error, errorContext);
3002
- };
3003
2657
  const visibility = context.visibility.hasAnyHiddenIntent ? context.visibility : undefined;
3004
2658
  const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
3005
2659
 
@@ -3011,11 +2665,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3011
2665
  //
3012
2666
  // Each segment routes through a World-owned Query with explicit read and
3013
2667
  // optional roles. The packed entity handle for `readWorldMat4Copy` /
3014
- // row reads come from `row.entity` -- the
2668
+ // `_getArrayView` reads comes from `row.entity` -- the
3015
2669
  // archetype-graph back-door (`graph.archetypes` / `arch.components.some`) is gone.
3016
2670
  // Plan-decisions K-2 sniffing scheme B (archetype-edge sniff once via
3017
- // `row.get(X) !== undefined`); K-3 invariant preserved (row access
2671
+ // `row.get(X) !== undefined`); K-3 invariant preserved (`_getArrayView`
3018
2672
  // calls survive untouched, only the entity source changes).
2673
+ const worldInternal = createWorldInternalView(world);
2674
+
3019
2675
  const cameras = extractCameraSnapshots(world);
3020
2676
 
3021
2677
  // Three-query union (M2 / w16 / AC-03): directional has no Transform
@@ -3042,14 +2698,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3042
2698
  | undefined;
3043
2699
  for (const row of directionalLightQuery) {
3044
2700
  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
2701
  directionalCount += 1;
3054
2702
  const intensity = l.intensity;
3055
2703
  const snapshot: DirectionalLightSnapshot = {
@@ -3092,24 +2740,21 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3092
2740
  .unwrap();
3093
2741
  for (const row of pointLightQuery) {
3094
2742
  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
2743
  // K-2 scheme B: archetype-edge sniff -- `bundle.Transform` key is absent
3100
2744
  // when the archetype does not carry the Transform column.
3101
- const transform = row.get(Transform);
2745
+ const hasTransform = row.get(Transform) !== undefined;
3102
2746
  const intensity = p.intensity;
3103
2747
  const range = p.range;
3104
2748
  const entityId = row.entity;
3105
2749
  // Position = world-space translation extracted from Transform.world.
3106
2750
  // A point light archetype without a Transform column sits at the origin.
3107
2751
  let worldMat: Float32Array | undefined;
3108
- if (transform !== undefined) worldMat = new Float32Array(transform.world);
2752
+ if (hasTransform) {
2753
+ const view = worldInternal._getArrayView(entityId, Transform, 'world');
2754
+ if (view !== undefined) worldMat = new Float32Array(view);
2755
+ }
3109
2756
  const position =
3110
- worldMat !== undefined
3111
- ? mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like)
3112
- : vec3.create(0, 0, 0);
2757
+ worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
3113
2758
  pointSnapshots.push({
3114
2759
  kind: 'point',
3115
2760
  position,
@@ -3137,21 +2782,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3137
2782
  let spotTileNext = 0;
3138
2783
  for (const row of spotLightQuery) {
3139
2784
  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);
2785
+ const hasTransform = row.get(Transform) !== undefined;
3145
2786
  const intensity = s.intensity;
3146
2787
  const range = s.range;
3147
2788
  const innerConeDeg = s.innerConeDeg;
3148
2789
  const outerConeDeg = s.outerConeDeg;
3149
2790
  let worldMat: Float32Array | undefined;
3150
- if (transform !== undefined) worldMat = new Float32Array(transform.world);
2791
+ if (hasTransform) {
2792
+ const entity = row.entity;
2793
+ const view = worldInternal._getArrayView(entity, Transform, 'world');
2794
+ if (view !== undefined) worldMat = new Float32Array(view);
2795
+ }
3151
2796
  const position =
3152
- worldMat !== undefined
3153
- ? mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like)
3154
- : vec3.create(0, 0, 0);
2797
+ worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
3155
2798
  const dir = vec3.create(s.direction[0] ?? 0, s.direction[1] ?? -1, s.direction[2] ?? 0);
3156
2799
 
3157
2800
  // Extract is the single direction-normalization owner for direct-light
@@ -3161,6 +2804,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3161
2804
  );
3162
2805
  const EPSILON = 1e-6;
3163
2806
  const hasValidDirection = dirLen > EPSILON;
2807
+ if (!hasValidDirection) {
2808
+ worldInternal._routeError(
2809
+ new SpawnLightInvalidBoundsError('SpotLight', 'direction', [
2810
+ dir[0] ?? 0,
2811
+ dir[1] ?? 0,
2812
+ dir[2] ?? 0,
2813
+ ]),
2814
+ {
2815
+ severity: Severity.Error,
2816
+ systemName: 'RenderSystem.extract (spot-direction)',
2817
+ },
2818
+ );
2819
+ }
3164
2820
  const dirN = vec3.create(
3165
2821
  hasValidDirection ? (dir[0] ?? 0) / dirLen : (dir[0] ?? 0),
3166
2822
  hasValidDirection ? (dir[1] ?? 0) / dirLen : (dir[1] ?? 0),
@@ -3203,7 +2859,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3203
2859
 
3204
2860
  spotSnapshots.push({
3205
2861
  kind: 'spot',
3206
- entity: row.entity,
3207
2862
  // D-6: position reflects world transform; direction stays sourced
3208
2863
  // from SpotLight.direction (NOT rotated by the parent).
3209
2864
  position,
@@ -3276,7 +2931,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3276
2931
  cascadeBlend: sf.cascadeBlend,
3277
2932
  mapSize: sf.mapSize,
3278
2933
  shadowDistance: sf.shadowDistance,
3279
- pcfKernelSize: sf.pcfKernelSize,
3280
2934
  };
3281
2935
  directionalCsmDirection = dirSnapshot.direction;
3282
2936
  const csm = computeDirectionalCsm(dirSnapshot.direction, directionalCsmConfig, cameraData);
@@ -3327,14 +2981,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3327
2981
  for (const row of pointShadowQuery) {
3328
2982
  const t = row.get(Transform);
3329
2983
  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
2984
  // Read world-space position from Transform.world (mat4 column-major;
3339
2985
  // translation lives at indices 12..14, mirroring CameraSnapshot.world
3340
2986
  // semantics in this file).
@@ -3400,7 +3046,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3400
3046
  }
3401
3047
  }
3402
3048
 
3403
- let lights: ExtractedLights = {
3049
+ const lights: ExtractedLights = {
3404
3050
  directional,
3405
3051
  directionalCount,
3406
3052
  point: pointSnapshots,
@@ -3485,56 +3131,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3485
3131
  }
3486
3132
  }
3487
3133
 
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
3134
  // feat-20260528-frustum-culling M3 / w10: precompute per-camera frustum
3539
3135
  // planes so entities can be tested against all cameras in the inner loop.
3540
3136
  // Cameras with degenerate projection parameters (e.g. zero fov, zero aspect)
@@ -3580,7 +3176,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3580
3176
  // carried in the world basis columns; the cull frustum uses the same view
3581
3177
  // the record stage derives, so cull stays same-source with render (AC-05).
3582
3178
  const view = mat4.create();
3583
- mat4.invert(view, cam.world as unknown as mat4.Mat4Like);
3179
+ mat4.invert(view, cam.world);
3584
3180
  const vp = mat4.create();
3585
3181
  mat4.multiply(vp, proj, view);
3586
3182
  const f = frustum.create();
@@ -3591,7 +3187,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3591
3187
 
3592
3188
  const renderables: RenderableSnapshot[] = [];
3593
3189
  // feat-20260528-frustum-culling M3 / w11: frustum culling counters.
3594
- const candidateRenderables: RenderableSnapshot[] = [];
3595
3190
  let frustumCulled = 0;
3596
3191
  let frustumTotal = 0;
3597
3192
  const explicitlyHidden = new Set<EntityHandle>();
@@ -3614,7 +3209,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3614
3209
  // (`row.get(X) !== undefined` edge sniff) replaces the prior
3615
3210
  // `arch.components.some` row-internal back-door. K-3 invariant: the
3616
3211
  // variable-length array reads (`MeshRenderer.materials`,
3617
- // `Instances.transforms`) still flow through row values /
3212
+ // `Instances.transforms`) still flow through `_getArrayView` /
3618
3213
  // `world.get(e, Instances)` -- only the `entity` source switches to
3619
3214
  // `row.entity`.
3620
3215
  //
@@ -3637,46 +3232,50 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3637
3232
  MeshFilter,
3638
3233
  Instances,
3639
3234
  Skin,
3640
- MorphWeights,
3641
3235
  Layer,
3236
+ MorphWeights,
3642
3237
  SpriteRegionOverride,
3643
3238
  SpriteInstances,
3644
3239
  ],
3645
3240
  })
3646
3241
  .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[] = [];
3242
+ // QueryRow deliberately exposes component data, not an archetype version.
3243
+ // The optimized ECS owner keeps table storage private, so instance-buffer
3244
+ // invalidation uses a content fingerprint of the managed arrays instead.
3245
+ const resolveArchVersion = (entity: EntityHandle): number => {
3246
+ const values = [
3247
+ worldInternal._getArrayView(entity, Instances, 'transforms'),
3248
+ worldInternal._getArrayView(entity, SpriteInstances, 'transforms'),
3249
+ worldInternal._getArrayView(entity, SpriteInstances, 'regions'),
3250
+ ];
3251
+ let hash = 2166136261;
3252
+ for (const value of values) {
3253
+ hash = Math.imul(hash ^ (value?.length ?? 0), 16777619) >>> 0;
3254
+ if (value === undefined) continue;
3255
+ for (let index = 0; index < value.length; index += 1) {
3256
+ hash = Math.imul(hash ^ Math.fround(value[index] ?? 0), 16777619) >>> 0;
3257
+ }
3258
+ }
3259
+ return hash;
3260
+ };
3261
+ // Pending entries are created with the current renderables.length, which is
3262
+ // exactly the slot this entity receives if it survives culling. No other
3263
+ // renderable can be pushed between staging and this entity's push, so publish
3264
+ // the original fresh entries instead of cloning every submesh descriptor.
3265
+ // A culled entity still discards its private pending array unchanged.
3659
3266
  const flushPendingDispatch = (pending: readonly DispatchEntry[]): void => {
3660
- for (const entry of pending) candidateDispatch.push(entry);
3267
+ for (const entry of pending) dispatch.push(entry);
3661
3268
  };
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) {
3269
+ for (const row of meshRendererQuery) {
3670
3270
  // K-2 archetype-edge sniff (scheme B): a missing optional component
3671
3271
  // surfaces as an absent bundle key, not a row-internal optional chain.
3672
3272
  // Presence-only checks use QueryRow.has so large array-bearing components
3673
3273
  // are not materialised merely to answer a boolean question.
3674
- const transformData = row.get(Transform);
3675
- const hasTransform = transformData !== undefined;
3274
+ const hasTransform = row.has(Transform);
3676
3275
  const meshFilter = row.get(MeshFilter);
3677
3276
  const hasInstances = row.has(Instances);
3678
3277
  const skin = row.get(Skin);
3679
- const morphWeights = row.get(MorphWeights)?.weights;
3278
+ const morphWeightsView = worldInternal._getArrayView(row.entity, MorphWeights, 'weights');
3680
3279
  const hasMeshFilter = meshFilter !== undefined;
3681
3280
  const hasSkin = skin !== undefined;
3682
3281
  // feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 / w10:
@@ -3705,8 +3304,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3705
3304
  // feat-20260608-tilemap-object-layer-rendering M3 / m3-t5: tilemap-spawned
3706
3305
  // per-cell render entities (the ones `tilemap-chunk-extract-system`
3707
3306
  // 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
3307
+ // archetype edge that carries a sprite entity -- they all wear
3308
+ // `MeshFilter.assetHandle === HANDLE_QUAD` + a `forgeax::sprite`-shaded
3710
3309
  // material asset + the sprite-bucket `values.region` rectangle.
3711
3310
  // For the per-entity Y-sort path (requirements §AC-12 / §AC-13):
3712
3311
  //
@@ -3721,7 +3320,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3721
3320
  // bucket types therefore feed one `transparentSortEntries` argsort
3722
3321
  // step + share the `argsortInPlace` radix LSD primitive (plan-strategy
3723
3322
  // §D-1 / §D-3). The detection lives on the material side -- detect a
3724
- // tilemap-spawned entity by its unit-quad MeshFilter
3323
+ // tilemap-spawned entity by `MeshFilter.assetHandle === HANDLE_QUAD`
3725
3324
  // plus the `forgeax::sprite` shader id on `MeshRenderer.material`'s
3726
3325
  // first pass + non-empty `values.region`; no new public ECS
3727
3326
  // marker component lands (charter F1 minimum surface).
@@ -3739,10 +3338,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3739
3338
  // sprite reduces the anchor budget to 0.5 rather than the asset's 1.0.
3740
3339
  //
3741
3340
  // SpriteRegionOverride.region is a fixed stride-4 value. Per-row reads
3742
- // route through row values for the zero-copy row window
3341
+ // route through `_getArrayView` for the zero-copy row window
3743
3342
  // 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).
3343
+ // reads -- K-3 carve-out keeps `_getArrayView` as the row-accessor of
3344
+ // record for each non-scalar column).
3746
3345
  const hasSpriteRegionOverride = row.has(SpriteRegionOverride);
3747
3346
  // feat-20260523-skin-skeleton-animation M2 / T-21: Skin component
3748
3347
  // column views for coexistence check + joint despawn fail-fast.
@@ -3754,11 +3353,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3754
3353
  if (hasInstances || hasSpriteInstances) {
3755
3354
  // All rows in this callback share one archetype. Only these two
3756
3355
  // instance-bearing paths consume the version in their cache key.
3757
- archVersion = 0;
3356
+ archVersion = resolveArchVersion(row.entity);
3758
3357
  }
3759
3358
 
3760
3359
  {
3761
- // feat-20260608 M2 / w11: read materials array via the component row
3360
+ // feat-20260608 M2 / w11: read materials array via _getArrayView
3762
3361
  const entity = row.entity;
3763
3362
  if (isRenderable && visibility?.effective(entity) === 'hidden') {
3764
3363
  explicitlyHidden.add(entity);
@@ -3777,9 +3376,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3777
3376
  // the deferred-shading smoke (PR #598 CI). Pure ordering fix; cull
3778
3377
  // logic and MeshRenderer contract unchanged.
3779
3378
  const pendingDispatch: DispatchEntry[] = [];
3780
- const candidateIndex = candidateRenderables.length;
3781
- const rendererValue = row.get(MeshRenderer);
3782
- const materialsView = rendererValue?.materials as unknown as Uint32Array | undefined;
3379
+ const materialsView = worldInternal._getArrayView(entity, MeshRenderer, 'materials') as
3380
+ | Uint32Array
3381
+ | undefined;
3783
3382
  const materialCount = materialsView?.length ?? 0;
3784
3383
  let materialHandles = Array.from(materialsView ?? []);
3785
3384
  let materialBindingSources: MeshMaterialBindingSource[] = materialHandles.map(
@@ -3798,7 +3397,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3798
3397
  assets !== null
3799
3398
  ) {
3800
3399
  const meshHandle = toShared<'MeshAsset'>(fAssetHandleVal);
3801
- const meshRes = resolveRenderAsset<Asset>(world, meshHandle);
3400
+ const meshRes = resolveAssetHandle<Asset>(world, meshHandle);
3802
3401
  if (meshRes.ok && meshRes.value.kind === 'mesh') {
3803
3402
  const meshAsset = meshRes.value as MeshAsset;
3804
3403
  gpuDrivenSubmeshes = meshAsset.submeshes;
@@ -3813,7 +3412,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3813
3412
  const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
3814
3413
  const resolvedBindings = resolveMeshMaterialBindings(meshAsset, materialsView ?? [], {
3815
3414
  isValidOverride(handle) {
3816
- const resolved = resolveRenderAsset<Asset>(world, toShared<'MaterialAsset'>(handle));
3415
+ const resolved = resolveAssetHandle(world, toShared<'MaterialAsset'>(handle));
3817
3416
  return resolved.ok && resolved.value.kind === 'material';
3818
3417
  },
3819
3418
  resolveMeshDefault(defaultGuid) {
@@ -3826,7 +3425,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3826
3425
  });
3827
3426
  if (!resolvedBindings.ok) {
3828
3427
  if (resolvedBindings.code === 'mesh-material-slots-missing') {
3829
- reportRenderError(
3428
+ worldInternal._routeError(
3830
3429
  new AssetError({
3831
3430
  code: 'load-failed',
3832
3431
  expected: 'every MeshAsset producer supplies materialSlots[]',
@@ -3837,19 +3436,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3837
3436
  subAssetGuid: '<material-slots-missing>',
3838
3437
  sourceField: { fieldName: 'materialSlots' },
3839
3438
  },
3840
- }) as unknown as Error,
3439
+ }),
3841
3440
  {
3441
+ severity: Severity.Error,
3842
3442
  systemName: 'RenderSystem.extract (mesh-material-slots-missing)',
3843
3443
  },
3844
3444
  );
3845
3445
  continue;
3846
3446
  }
3847
3447
  const materialGuid = AssetGuid.format(resolvedBindings.defaultMaterial);
3848
- reportRenderError(
3448
+ worldInternal._routeError(
3849
3449
  new AssetError({
3850
3450
  code: 'load-failed',
3851
3451
  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}`,
3452
+ hint: `loadByGuid(meshGuid) must recursively load the declared default material; mesh=${guid}, slot=${resolvedBindings.slotIndex}, material=${materialGuid}`,
3853
3453
  detail: {
3854
3454
  referencedByGuid: guid,
3855
3455
  referencedByKind: 'mesh',
@@ -3859,8 +3459,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3859
3459
  arrayIndex: resolvedBindings.slotIndex,
3860
3460
  },
3861
3461
  },
3862
- }) as unknown as Error,
3462
+ }),
3863
3463
  {
3464
+ severity: Severity.Error,
3864
3465
  systemName: 'RenderSystem.extract (mesh-default-not-ready)',
3865
3466
  },
3866
3467
  );
@@ -3884,7 +3485,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3884
3485
  },
3885
3486
  }));
3886
3487
  for (const diagnostic of resolvedBindings.diagnostics) {
3887
- reportRenderError(
3488
+ worldInternal._routeError(
3888
3489
  new AssetError({
3889
3490
  code: diagnostic.code,
3890
3491
  expected:
@@ -3904,8 +3505,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3904
3505
  slotIndex: diagnostic.slotIndex,
3905
3506
  handle: diagnostic.handle ?? 0,
3906
3507
  },
3907
- }) as unknown as Error,
3508
+ }),
3908
3509
  {
3510
+ severity: Severity.Warning,
3909
3511
  systemName: `RenderSystem.extract (${diagnostic.code})`,
3910
3512
  },
3911
3513
  );
@@ -3941,7 +3543,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3941
3543
  cachedMaterial.passes,
3942
3544
  entity,
3943
3545
  handleRaw,
3944
- candidateIndex,
3546
+ renderables.length,
3945
3547
  layerVal,
3946
3548
  materialSnap.paramSnapshot,
3947
3549
  );
@@ -3968,22 +3570,23 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3968
3570
  stablePersistentCached.passes,
3969
3571
  entity,
3970
3572
  handleRaw,
3971
- candidateIndex,
3573
+ renderables.length,
3972
3574
  layerVal,
3973
3575
  materialSnap.paramSnapshot,
3974
3576
  );
3975
3577
  }
3976
3578
  } else {
3977
- const res = resolveRenderAsset<Asset>(world, tagged);
3579
+ const res = resolveAssetHandle(world, tagged);
3978
3580
  if (!res.ok) {
3979
3581
  if (isRenderable) {
3980
3582
  const rhiErr = new RhiError({
3981
3583
  code: 'asset-not-registered',
3982
- expected: 'MeshRenderer.material in RenderAssetPort',
3584
+ expected: 'MeshRenderer.material in AssetRegistry',
3983
3585
  hint: 'catalog the material via assetRegistry.catalog(guid, asset) + world.allocSharedRef before spawn, or remove the material field to fall back to default',
3984
3586
  detail: { assetHandle: handleRaw },
3985
3587
  });
3986
- reportRenderError(rhiErr as unknown as Error, {
3588
+ worldInternal._routeError(rhiErr, {
3589
+ severity: Severity.Error,
3987
3590
  systemName: 'RenderSystem.extract (material asset-not-registered)',
3988
3591
  });
3989
3592
  }
@@ -3993,12 +3596,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3993
3596
  if (asset.kind !== 'material') {
3994
3597
  materialSnap = defaultMaterialSnapshot(handleRaw);
3995
3598
  } else {
3996
- const materialGuid = assets.guidOf(asset);
3997
3599
  // feat-20260529 M3 / w11: material parent chain inheritance via
3998
3600
  // read-through _materialWalk accessor (plan-strategy D-6).
3999
3601
  // The old direct asset.passes / asset.values read never
4000
3602
  // walked the parent chain, causing broken-inheritance (root cause).
4001
- const resolvedResult = resolveMaterialFromPort(asset, assets);
3603
+ const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assets);
4002
3604
  if (!resolvedResult.ok) {
4003
3605
  // AC-09 / S-7 / q8=A: passes-empty or cycle must fire structured
4004
3606
  // error through _routeError (same routing as asset-not-registered
@@ -4011,26 +3613,29 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4011
3613
  case 'material-value-unknown':
4012
3614
  case 'material-value-type-mismatch':
4013
3615
  case 'material-contract-program-mismatch':
4014
- reportRenderError(err as unknown as Error, {
3616
+ worldInternal._routeError(err, {
3617
+ severity: Severity.Error,
4015
3618
  systemName: `RenderSystem.extract (${err.code})`,
4016
3619
  });
4017
3620
  break;
4018
3621
  case 'material-circular-inheritance':
4019
- reportRenderError(err as unknown as Error, {
3622
+ worldInternal._routeError(err, {
3623
+ severity: Severity.Error,
4020
3624
  systemName: 'RenderSystem.extract (material-circular-inheritance)',
4021
3625
  });
4022
3626
  break;
4023
3627
  default:
4024
3628
  // Exhaustive guard: unhandled error codes from _materialWalk
4025
3629
  // surface an internal assertion to avoid silent continuation.
4026
- reportRenderError(err as unknown as Error, {
3630
+ worldInternal._routeError(err, {
3631
+ severity: Severity.Error,
4027
3632
  systemName: `RenderSystem.extract (_materialWalk: ${err.code})`,
4028
3633
  });
4029
3634
  }
4030
3635
  continue;
4031
3636
  }
4032
3637
  const resolved = resolvedResult.value;
4033
- const allPasses = resolved.passes ?? [];
3638
+ const allPasses = resolved.passes;
4034
3639
  const firstPassShader =
4035
3640
  allPasses.length > 0
4036
3641
  ? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
@@ -4064,50 +3669,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4064
3669
  }
4065
3670
  }
4066
3671
 
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
- }
3672
+ const materialParamSchema = materialParametersToParamSchema(
3673
+ resolved.parameters ?? [],
3674
+ firstPassShader,
3675
+ );
4111
3676
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
4112
3677
  // bidirectional Skin <-> pbr-skin material fail-fast at extract.
4113
3678
  // Skin component without a forgeax::pbr-skin first-pass material
@@ -4126,20 +3691,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4126
3691
  skinSkeletonView !== 0;
4127
3692
  const isPbrSkinMaterial = firstPassShader === 'forgeax::pbr-skin';
4128
3693
  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
- );
3694
+ worldInternal._routeError(new SkinMaterialMismatchError(entity, firstPassShader), {
3695
+ severity: Severity.Error,
3696
+ systemName: 'RenderSystem.extract (skin-material-mismatch)',
3697
+ });
4138
3698
  continue;
4139
3699
  }
4140
3700
  if (isPbrSkinMaterial && fAssetHandleVal !== undefined && fAssetHandleVal !== 0) {
4141
3701
  const meshHandleForSkinCheck = toShared<'MeshAsset'>(fAssetHandleVal);
4142
- const meshResForSkinCheck = resolveRenderAsset<MeshAsset>(
3702
+ const meshResForSkinCheck = resolveAssetHandle<MeshAsset>(
4143
3703
  world,
4144
3704
  meshHandleForSkinCheck,
4145
3705
  );
@@ -4150,15 +3710,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4150
3710
  if (!hasSkinIdx || !hasSkinWt) {
4151
3711
  const missing: 'skinIndex' | 'skinWeight' | 'both' =
4152
3712
  !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
- );
3713
+ worldInternal._routeError(new MaterialSkinAttrMissingError(entity, missing), {
3714
+ severity: Severity.Error,
3715
+ systemName: 'RenderSystem.extract (material-skin-attr-missing)',
3716
+ });
4162
3717
  continue;
4163
3718
  }
4164
3719
  }
@@ -4183,32 +3738,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4183
3738
  const isSprite =
4184
3739
  firstPassShader === 'forgeax::sprite' || firstPassShader === 'forgeax::sprite-lit';
4185
3740
 
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
3741
  // feat-20260613-material-paramschema-driven-binding M4 / w23
4213
3742
  // (D-5 graceful): paramSchema-driven texture-field validation.
4214
3743
  // For each handle-shaped paramValue (typeof === 'number'),
@@ -4232,7 +3761,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4232
3761
  if (assets === null || assets === undefined) return undefined;
4233
3762
  // M4: intern so the GUID mints one stable handle per World
4234
3763
  // instead of a fresh slot every frame (GPU residency relies on
4235
- // a stable handleSlot). Final release is owner evidence.
3764
+ // a stable handleSlot). onLastRelease -> gpuStore.evictTexture.
4236
3765
  const interned = internSharedRefFromGuid(
4237
3766
  world,
4238
3767
  assets,
@@ -4242,34 +3771,34 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4242
3771
  if (interned === undefined) return undefined;
4243
3772
  handle = interned;
4244
3773
  } else if (typeof textureRef === 'number') {
4245
- handle = textureRef as unknown as Handle<'TextureAsset', 'shared'>;
3774
+ handle = toShared<'TextureAsset'>(textureRef);
4246
3775
  } else {
4247
3776
  return undefined;
4248
3777
  }
4249
3778
  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));
3779
+ const declaredFields = materialTextureFields(
3780
+ firstPassShader,
3781
+ materialParamSchema.length > 0
3782
+ ? derive(materialParamSchema).textureFieldNames
3783
+ : firstPassShader !== undefined
3784
+ ? assets.materialShaderTextureFieldNames(firstPassShader)
3785
+ : undefined,
3786
+ );
4257
3787
  // Shader not registered (R-4 cross-worktree path) -> trust the
4258
- // raw handle and let the record stage / GPU layer surface any
3788
+ // raw handle and let the record stage / GPU layer surface each
4259
3789
  // mismatch via MISSING_TEXTURE_HANDLE.
4260
3790
  if (declaredFields === undefined) return handle;
4261
3791
  // Field is not declared as a texture by the shader -> the
4262
3792
  // loader's "try every int" fallback misclassified a scalar;
4263
3793
  // 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) {
3794
+ if (
3795
+ !declaredFields.has(fieldName) &&
3796
+ !isEngineInjectedTextureField(firstPassShader, fieldName)
3797
+ ) {
4269
3798
  return undefined;
4270
3799
  }
4271
3800
  // Field declared as texture: verify the handle's asset kind.
4272
- const assetRes = resolveRenderAsset<Asset>(world, handle);
3801
+ const assetRes = resolveAssetHandle(world, handle);
4273
3802
  if (!assetRes.ok) return undefined;
4274
3803
  const kind = (assetRes.value as { kind?: string }).kind;
4275
3804
  if (kind !== 'texture') return undefined;
@@ -4283,7 +3812,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4283
3812
  brand: B,
4284
3813
  ): Handle<B, 'shared'> | undefined => {
4285
3814
  const value = materialTextureRef(raw);
4286
- if (typeof value === 'number') return value as unknown as Handle<B, 'shared'>;
3815
+ if (typeof value === 'number') return toShared<B>(value);
4287
3816
  const guid = assetReferenceText(value);
4288
3817
  if (guid !== undefined) {
4289
3818
  if (assets === null || assets === undefined) return undefined;
@@ -4299,15 +3828,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4299
3828
  // carried, replacing the hardcoded 3-field list. validateTextureHandle
4300
3829
  // already drops fields a shader doesn't declare as a texture.
4301
3830
  const userRegionFields =
4302
- derivedMaterialInterface !== undefined
4303
- ? derivedMaterialInterface.textureFieldNames
4304
- : (emptySchemaTextureFields ??
4305
- (firstPassShader !== undefined && assets !== null && assets !== undefined
3831
+ materialTextureFields(
3832
+ firstPassShader,
3833
+ materialParamSchema.length > 0
3834
+ ? derive(materialParamSchema).textureFieldNames
3835
+ : firstPassShader !== undefined && assets !== null && assets !== undefined
4306
3836
  ? assets.materialShaderTextureFieldNames(firstPassShader)
4307
- : undefined));
3837
+ : undefined,
3838
+ ) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
4308
3839
  const textureHandles = new Map<string, Handle<'TextureAsset', 'shared'>>();
4309
3840
  const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
4310
- for (const field of userRegionFields ?? []) {
3841
+ for (const field of userRegionFields) {
4311
3842
  // D-5: a video-kind paramValue routes to the transient path
4312
3843
  // (videoTextureFields), NOT validateTextureHandle (which drops
4313
3844
  // kind!=='texture', the R-7 silent-fail path). Static fields fall
@@ -4329,20 +3860,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4329
3860
  const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
4330
3861
  resolveParamHandle(materialTextureRef(value), 'SamplerAsset'),
4331
3862
  );
4332
- const emissiveTextureHandle = textureHandles.get('emissiveTexture');
4333
- const occlusionTextureHandle = textureHandles.get('occlusionTexture');
4334
- const textureCoordinates = collectMaterialTextureCoordinates(
4335
- pv,
4336
- materialParamSchema,
4337
- derivedMaterialInterface,
3863
+ const emissiveTextureHandle = validateTextureHandle(
3864
+ 'emissiveTexture',
3865
+ pv.emissiveTexture,
4338
3866
  );
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
- }
3867
+ const occlusionTextureHandle = validateTextureHandle(
3868
+ 'occlusionTexture',
3869
+ pv.occlusionTexture,
3870
+ );
3871
+ const textureCoordinates = collectMaterialTextureCoordinates(pv);
4346
3872
  const emissivePv = pv.emissive as readonly number[] | undefined;
4347
3873
  // feat-20260625 M2 / w6: first-pass transparency flag folds into
4348
3874
  // MaterialSnapshot.transparent so the record stage can drive the
@@ -4369,10 +3895,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4369
3895
  // SpriteRegionOverride: per-entity per-frame region displacement.
4370
3896
  let overrideRegion: readonly [number, number, number, number] | undefined;
4371
3897
  if (hasSpriteRegionOverride) {
4372
- const overrideValue = world.get(entity, SpriteRegionOverride);
4373
- const overrideView = overrideValue.ok
4374
- ? (overrideValue.value.region as unknown as Float32Array)
4375
- : undefined;
3898
+ const overrideView = worldInternal._getArrayView(
3899
+ entity,
3900
+ SpriteRegionOverride,
3901
+ 'region',
3902
+ ) as Float32Array | undefined;
4376
3903
  if (overrideView !== undefined && overrideView.length >= 4) {
4377
3904
  overrideRegion = [
4378
3905
  overrideView[0] ?? 0,
@@ -4402,36 +3929,18 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4402
3929
  regionY += regionW;
4403
3930
  regionW = -regionW;
4404
3931
  }
4405
- paramSnap.region = [regionX, regionY, regionZ, regionW] as unknown as number[];
3932
+ paramSnap.region = [regionX, regionY, regionZ, regionW];
4406
3933
  // Guard: slicesAndMode must be present and zero for non-9-slice
4407
3934
  // 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.
3935
+ // writes [0,0,0,0] at offset 48 instead of leaving the
3936
+ // buildPbrMaterialUboPayload PBR baseline (e.g. occlusionStrength=1
3937
+ // at that slot). A non-zero slicesAndMode trips `useSlices=true`
3938
+ // in sprite.wgsl, which degenerates HANDLE_QUAD geometry invisible.
4412
3939
  if (!('slicesAndMode' in paramSnap)) {
4413
3940
  (paramSnap as Record<string, unknown>).slicesAndMode = [0, 0, 0, 0];
4414
3941
  }
4415
3942
  }
4416
3943
 
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
3944
  // Generic materialShaderId snapshot --- sprite included now flows
4436
3945
  // through this single branch (plan-strategy D-3 / AC-01 / AC-02 /
4437
3946
  // AC-07). The sprite block above only writes paramSnap.region (D-8
@@ -4452,18 +3961,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4452
3961
  }),
4453
3962
  normalScale: normalScalePv,
4454
3963
  materialShaderId: firstPassShader,
4455
- ...(materialGuid === undefined ? {} : { materialGuid }),
4456
3964
  materialHandle: handleRaw,
4457
- shadowCaster: hasShadowCasterPass(allPasses),
4458
3965
  renderState: pipelineRenderState(allPasses[0]?.renderState),
4459
3966
  paramSnapshot: paramSnap,
4460
3967
  ...(materialParamSchema.length > 0 && { materialParamSchema }),
4461
- ...(derivedMaterialSlot?.ok === true && {
4462
- derivedMaterialSlot: derivedMaterialSlot.value,
4463
- }),
4464
- ...(derivedMaterialSlot?.ok === false && {
4465
- derivedMaterialError: derivedMaterialSlot.error,
4466
- }),
4467
3968
  ...(textureCoordinates.size > 0 && { textureCoordinates }),
4468
3969
  ...(samplerHandles.size > 0 && { samplerHandles }),
4469
3970
  ...(textureHandles.size > 0 && { textureHandles }),
@@ -4516,7 +4017,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4516
4017
  allPasses,
4517
4018
  entity,
4518
4019
  handleRaw,
4519
- candidateIndex,
4020
+ renderables.length,
4520
4021
  layerVal,
4521
4022
  paramSnap,
4522
4023
  );
@@ -4550,7 +4051,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4550
4051
  // handleRaw===0 guard alone preserves the exact same dispatch shape.
4551
4052
  if (isRenderable && handleRaw === 0) {
4552
4053
  const shadowCasterTags: Record<string, string> = { LightMode: 'ShadowCaster' };
4553
- const nextRenderableIndex = candidateIndex;
4054
+ const nextRenderableIndex = renderables.length;
4554
4055
  // M2.5: stage into pendingDispatch; flushed at the renderable push
4555
4056
  // site below only when the entity survives cull.
4556
4057
  pendingDispatch.push({
@@ -4558,7 +4059,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4558
4059
  materialHandle: 0,
4559
4060
  renderableIndex: nextRenderableIndex,
4560
4061
  passIndex: 0,
4561
- queue: RenderQueue.Geometry,
4062
+ queue: 2000,
4562
4063
  layer: layerVal,
4563
4064
  tags: shadowCasterTags,
4564
4065
  renderState: undefined,
@@ -4570,22 +4071,16 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4570
4071
  });
4571
4072
  // Also add a Forward pass entry so the entity renders in the
4572
4073
  // 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
- });
4074
+ appendMaterialDispatchEntries(
4075
+ pendingDispatch,
4076
+ [DEFAULT_FORWARD_PASS],
4077
+ entity,
4078
+ 0,
4079
+ nextRenderableIndex,
4080
+ layerVal,
4081
+ {},
4082
+ 1,
4083
+ );
4589
4084
  }
4590
4085
 
4591
4086
  if (isRenderable) {
@@ -4605,30 +4100,21 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4605
4100
  ) {
4606
4101
  // Skin + Instances coexistence is forbidden (D-10).
4607
4102
  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
- );
4103
+ worldInternal._routeError(new SkinInstancesCoexistForbiddenError(entity), {
4104
+ severity: Severity.Error,
4105
+ systemName: 'RenderSystem.extract (skin-instances-coexist)',
4106
+ });
4616
4107
 
4617
4108
  continue;
4618
4109
  }
4619
4110
  // (a) Resolve skeleton asset; on failure -> skeleton-resolve-failed.
4620
4111
  const skeletonHandle = toShared<'SkeletonAsset'>(skeletonHandleRaw);
4621
- const skeletonRes = resolveRenderAsset<SkeletonAsset>(world, skeletonHandle);
4112
+ const skeletonRes = resolveAssetHandle<SkeletonAsset>(world, skeletonHandle);
4622
4113
  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
- );
4114
+ worldInternal._routeError(new SkeletonResolveFailedError(entity, skeletonHandleRaw), {
4115
+ severity: Severity.Error,
4116
+ systemName: 'RenderSystem.extract (skeleton-resolve-failed)',
4117
+ });
4632
4118
  continue;
4633
4119
  }
4634
4120
  const skeleton = skeletonRes.value;
@@ -4638,20 +4124,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4638
4124
  if (skinJoints === undefined) continue;
4639
4125
  const jointsLength = skinJoints.length;
4640
4126
  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,
4127
+ worldInternal._routeError(
4128
+ new JointCountMismatchError(entity, skeleton.jointCount, jointsLength),
4647
4129
  {
4130
+ severity: Severity.Error,
4648
4131
  systemName: 'RenderSystem.extract (joint-count-mismatch)',
4649
4132
  },
4650
4133
  );
4651
4134
  continue;
4652
4135
  }
4653
4136
  // (c) Resolve only each joint's Transform.world column. The
4654
- // transient view is consumed before any structural mutation and
4137
+ // transient view is consumed before a structural mutation and
4655
4138
  // therefore preserves the same dangling-joint behavior without
4656
4139
  // constructing every other Transform field.
4657
4140
  // Build mat4 list eagerly so write happens once per entity (no
@@ -4660,27 +4143,24 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4660
4143
  let jointDangling = -1;
4661
4144
  for (let jIdx = 0; jIdx < skeleton.jointCount; jIdx++) {
4662
4145
  const jointEntityRaw = skinJoints[jIdx] ?? 0;
4663
- const jointEntity = jointEntityRaw as unknown as EntityHandle;
4664
- const jointTransform = world.get(jointEntity, Transform);
4665
- if (!jointTransform.ok) {
4146
+ const jointEntity = jointEntityRaw as EntityHandle;
4147
+ const jointWorld = worldInternal._getArrayView(jointEntity, Transform, 'world');
4148
+ if (jointWorld === undefined) {
4666
4149
  jointDangling = jIdx;
4667
4150
  break;
4668
4151
  }
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;
4152
+ // The allocator consumes the canonical Mat4 brand. Copy the
4153
+ // column view into that owner-created value so the ECS storage
4154
+ // view never crosses the math brand boundary by assertion.
4155
+ const jointWorldMat = mat4.create();
4156
+ jointWorldMat.set(jointWorld);
4157
+ jointWorlds[jIdx] = jointWorldMat;
4673
4158
  }
4674
4159
  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
- );
4160
+ worldInternal._routeError(new JointEntityDanglingError(entity, jointDangling), {
4161
+ severity: Severity.Error,
4162
+ systemName: 'RenderSystem.extract (joint-entity-dangling)',
4163
+ });
4684
4164
  continue;
4685
4165
  }
4686
4166
  // (d) Slice the IBM flat Float32Array into per-joint Float32Arrays.
@@ -4699,7 +4179,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4699
4179
  skinPaletteAllocator.writeJointPalette(slice, ibms, jointWorlds);
4700
4180
  skinSlice = {
4701
4181
  jointCount: slice.jointCount,
4702
- jointMatrices: new Float32Array(slice.jointMatrices),
4703
4182
  byteOffset: slice.byteOffset,
4704
4183
  buffer: slice.buffer,
4705
4184
  };
@@ -4712,14 +4191,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4712
4191
  // record stage copies these 16 floats into the mesh SSBO with zero
4713
4192
  // per-snapshot `mat4.compose` (AC-07). A stale slot (generation gone)
4714
4193
  // skips the renderable, mirroring the Instances dangling-row sweep.
4715
- // tweak-20260611 M1 / K-3: row access stays zero-copy;
4194
+ // tweak-20260611 M1 / K-3: `_getArrayView` call survives untouched;
4716
4195
  // 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;
4196
+ const worldView = worldInternal._getArrayView(entity, Transform, 'world');
4197
+ if (worldView === undefined) continue;
4198
+ const worldMat = new Float32Array(worldView);
4723
4199
  const transformSnap: TransformSnapshot = { world: worldMat };
4724
4200
  // feat-20260608 M5 amend / w11-a: per-submesh `materials[]` array
4725
4201
  // aligned 1-1 with `MeshAsset.submeshes[]`. materials[0] === the
@@ -4741,14 +4217,30 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4741
4217
  subHandle,
4742
4218
  world,
4743
4219
  assets,
4744
- gpuStore,
4745
4220
  materialSnapshotCache,
4746
4221
  persistentMaterialSnapshotCache,
4747
- context.paramSchemaProjectionCensus,
4748
4222
  ),
4749
4223
  );
4750
4224
  }
4751
4225
  }
4226
+ if (isRenderable && assets !== undefined && assets !== null) {
4227
+ for (let mi = 1; mi < materialHandles.length; mi++) {
4228
+ const subHandle = materialHandles[mi] ?? 0;
4229
+ if (subHandle === handleRaw) continue;
4230
+ const subEntry =
4231
+ materialSnapshotCache.get(subHandle) ??
4232
+ readPersistentMaterialSnapshot(persistentMaterialSnapshotCache, subHandle, assets);
4233
+ appendMaterialDispatchEntries(
4234
+ pendingDispatch,
4235
+ subEntry?.passes ?? (subHandle === 0 ? [DEFAULT_FORWARD_PASS] : []),
4236
+ entity,
4237
+ subHandle,
4238
+ renderables.length,
4239
+ layerVal,
4240
+ materialsArr[mi]?.paramSnapshot,
4241
+ );
4242
+ }
4243
+ }
4752
4244
 
4753
4245
  // feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 /
4754
4246
  // w10: SpriteInstances validation + snapshot materialisation.
@@ -4758,11 +4250,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4758
4250
  if (hasSpriteInstances) {
4759
4251
  // (1) mutually exclusive with Instances (peers — pick one).
4760
4252
  if (hasInstances) {
4761
- reportRenderError(
4762
- new SpriteInstancesMutuallyExclusiveWithInstancesError(
4763
- entity as unknown as number,
4764
- ) as unknown as Error,
4253
+ worldInternal._routeError(
4254
+ new SpriteInstancesMutuallyExclusiveWithInstancesError(entity),
4765
4255
  {
4256
+ severity: Severity.Error,
4766
4257
  systemName: 'RenderSystem.extract (sprite-instances-mutually-exclusive)',
4767
4258
  },
4768
4259
  );
@@ -4781,12 +4272,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4781
4272
  materialSnap.materialShaderId !== 'forgeax::sprite' &&
4782
4273
  materialSnap.materialShaderId !== 'forgeax::sprite-lit'
4783
4274
  ) {
4784
- reportRenderError(
4275
+ worldInternal._routeError(
4785
4276
  new SpriteInstancesRequiresSpriteShaderError(
4786
- entity as unknown as number,
4277
+ entity,
4787
4278
  materialSnap.materialShaderId ?? 'undefined',
4788
- ) as unknown as Error,
4279
+ ),
4789
4280
  {
4281
+ severity: Severity.Error,
4790
4282
  systemName: 'RenderSystem.extract (sprite-instances-requires-sprite-shader)',
4791
4283
  },
4792
4284
  );
@@ -4795,9 +4287,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4795
4287
  // (3) count mismatch — transforms.length / 16 === regions.length / 4
4796
4288
  // (transforms.length=0 + regions.length=0 is the zero-instance lawful
4797
4289
  // 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;
4290
+ const transforms = worldInternal._getArrayView(entity, SpriteInstances, 'transforms');
4291
+ const regions = worldInternal._getArrayView(entity, SpriteInstances, 'regions');
4801
4292
  if (transforms !== undefined && regions !== undefined) {
4802
4293
  const transformsLength = transforms.length;
4803
4294
  const regionsLength = regions.length;
@@ -4808,12 +4299,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4808
4299
  const tCount = transformsLength / 16;
4809
4300
  const rCount = regionsLength / 4;
4810
4301
  if (transformsLength % 16 !== 0 || regionsLength % 4 !== 0 || tCount !== rCount) {
4811
- reportRenderError(
4812
- new SpriteInstancesCountMismatchError(
4813
- transformsLength,
4814
- regionsLength,
4815
- ) as unknown as Error,
4302
+ worldInternal._routeError(
4303
+ new SpriteInstancesCountMismatchError(transformsLength, regionsLength),
4816
4304
  {
4305
+ severity: Severity.Error,
4817
4306
  systemName: 'RenderSystem.extract (sprite-instances-count-mismatch)',
4818
4307
  },
4819
4308
  );
@@ -4828,35 +4317,39 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4828
4317
  transforms: transformsCopy,
4829
4318
  regions: regionsCopy,
4830
4319
  instanceCount: tCount,
4831
- cacheKey: entity as unknown as number,
4320
+ cacheKey: entity,
4832
4321
  archVersion,
4833
4322
  };
4834
4323
  }
4835
4324
  }
4836
4325
 
4837
4326
  let localAabb: Float32Array | undefined;
4327
+ let morph: MorphSnapshot | undefined;
4838
4328
  const assetHandleRaw = Math.round(fAssetHandle ?? 0);
4839
4329
  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);
4330
+ const meshRes = resolveAssetHandle(world, toShared<'MeshAsset'>(assetHandleRaw));
4331
+ if (meshRes.ok && meshRes.value.kind === 'mesh') {
4332
+ const meshAsset = meshRes.value as MeshAsset;
4333
+ morph = morphSnapshotFor(meshAsset, morphWeightsView);
4334
+ const meshAabb = meshAsset.aabb;
4335
+ // Morph targets can expand the authored bounds. Keep the
4336
+ // specialized lane conservative until a target-aware bounds
4337
+ // projection is available; a false-positive draw is preferable
4338
+ // to culling a valid deformed vertex.
4339
+ if (morph === undefined && hasFiniteOrderedLocalAabb(meshAabb)) {
4340
+ localAabb = new Float32Array(meshAabb);
4341
+ }
4342
+ }
4846
4343
  }
4847
4344
 
4848
4345
  let nonIndexedFirst = 0;
4849
- const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh, submeshIndex) => {
4346
+ const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh) => {
4850
4347
  const drawMaterial = materialsArr[submesh.materialSlot] ?? materialSnap;
4851
- const forwardPass = gpuDrivenForwardPass(drawMaterial, materialSnapshotCache);
4852
4348
  const first = gpuDrivenIndexed ? submesh.indexOffset : nonIndexedFirst;
4853
4349
  nonIndexedFirst += submesh.vertexCount;
4854
- if (drawMaterial.transparent === true || forwardPass === undefined) return [];
4350
+ if (drawMaterial.transparent === true) return [];
4855
4351
  return [
4856
4352
  {
4857
- submeshIndex,
4858
- passIndex: forwardPass.passIndex,
4859
- queue: forwardPass.queue,
4860
4353
  kind: gpuDrivenIndexed ? ('indexed' as const) : ('non-indexed' as const),
4861
4354
  first,
4862
4355
  count: gpuDrivenIndexed ? submesh.indexCount : submesh.vertexCount,
@@ -4871,53 +4364,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4871
4364
  const baseRenderable: RenderableSnapshot = {
4872
4365
  assetHandle: Math.round(fAssetHandle ?? 0),
4873
4366
  transform: transformSnap,
4874
- ...(boundsWorld !== undefined ? { boundsWorld } : {}),
4875
- ...(candidateInstanceTransforms !== undefined
4876
- ? { candidateInstanceTransforms: new Float32Array(candidateInstanceTransforms) }
4877
- : {}),
4878
4367
  ...(localAabb !== undefined ? { localAabb } : {}),
4879
4368
  material: materialSnap,
4880
4369
  materials: materialsArr,
4881
4370
  materialBindingSources,
4882
4371
  materialBindingDiagnostics,
4883
- ...(gpuDrivenDraws.length === 0 ? {} : { gpuDrivenDraws }),
4372
+ ...(morph === undefined && gpuDrivenDraws.length > 0 ? { gpuDrivenDraws } : {}),
4884
4373
  worldId: 0,
4885
- entityKey: entity as unknown as number,
4886
- candidateIndex,
4374
+ entityKey: entity,
4887
4375
  ...(skinSlice !== undefined ? { skin: skinSlice } : {}),
4888
- ...(morphWeights === undefined ? {} : { morphWeights: new Float32Array(morphWeights) }),
4376
+ ...(morph !== undefined ? { morph } : {}),
4889
4377
  ...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),
4890
4378
  };
4891
4379
 
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
4380
  // feat-20260528-frustum-culling M3 / w10: frustum culling check.
4922
4381
  // Skip the entity if a valid AABB exists AND ALL cameras' frusta
4923
4382
  // reject the world-space AABB. Missing or malformed AABBs are
@@ -4929,13 +4388,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4929
4388
  // directly (no compose) -- same source the record stage feeds
4930
4389
  // the mesh SSBO, so cull stays same-source with render (AC-05).
4931
4390
  const worldAabb = box3.create();
4932
- box3.transformBox3(
4933
- worldAabb,
4934
- localAabb,
4935
- transformSnap.world as unknown as Parameters<typeof box3.transformBox3>[2],
4936
- );
4391
+ box3.transformBox3(worldAabb, localAabb, transformSnap.world);
4937
4392
 
4938
- // Test against all cameras. Entity is visible if any camera
4393
+ // Test against all cameras. Entity is visible if one camera
4939
4394
  // frustum intersects the world-space AABB (or planes are empty
4940
4395
  // from degenerate projection).
4941
4396
  frustumTotal += 1;
@@ -4953,13 +4408,42 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4953
4408
  }
4954
4409
  if (!visible) {
4955
4410
  frustumCulled += 1;
4956
- for (const entry of pendingDispatch) candidateDispatch.push(entry);
4957
4411
  continue;
4958
4412
  }
4959
4413
  }
4960
4414
 
4961
- flushPendingDispatch(pendingDispatch);
4962
- renderables.push(publishedRenderable);
4415
+ if (hasInstances) {
4416
+ const transforms = worldInternal._getArrayView(entity, Instances, 'transforms');
4417
+ if (transforms === undefined) {
4418
+ flushPendingDispatch(pendingDispatch);
4419
+ renderables.push(baseRenderable);
4420
+ } else {
4421
+ const actualLength = transforms.length;
4422
+ if (actualLength % 16 !== 0) {
4423
+ worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
4424
+ severity: Severity.Error,
4425
+ systemName: 'RenderSystem.extract (Instances stride)',
4426
+ });
4427
+
4428
+ continue;
4429
+ }
4430
+ const snapshotCopy = new Float32Array(transforms);
4431
+ const instanceCount = Math.max(1, Math.floor(actualLength / 16));
4432
+ flushPendingDispatch(pendingDispatch);
4433
+ renderables.push({
4434
+ ...baseRenderable,
4435
+ instances: {
4436
+ transforms: snapshotCopy,
4437
+ instanceCount,
4438
+ cacheKey: entity,
4439
+ archVersion,
4440
+ },
4441
+ });
4442
+ }
4443
+ } else {
4444
+ flushPendingDispatch(pendingDispatch);
4445
+ renderables.push(baseRenderable);
4446
+ }
4963
4447
  }
4964
4448
 
4965
4449
  // feat-20260520-2d-sprite-layer-mvp M-3 / w22 + w25: finalise the
@@ -4973,53 +4457,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4973
4457
  }
4974
4458
  }
4975
4459
 
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
- };
4460
+ // M3 / w26: sort dispatch entries by queue (ascending, stable sort)
4461
+ // per plan-strategy D-3.
4462
+ dispatch = sortDispatchByQueue(dispatch);
5023
4463
 
5024
4464
  // D-1: collect PostProcessParams entities into Map<shaderId, Uint8Array>.
5025
4465
  // Last-one-wins when multiple entities bear the same shader id (mirrors
@@ -5037,17 +4477,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5037
4477
  // provider. The engine bridges the active camera's `Camera.exposure /
5038
4478
  // whitePoint / tonemap` onto the SAME unified params channel custom
5039
4479
  // 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
4480
+ // the engine itself acts as the provider for the Standard tonemap shader
5041
4481
  // id. The 16B layout is byte-identical to the prior recordTonemapPass packing
5042
4482
  // (render-system-record.ts pre-w14): Float32 [exposure, whitePoint, _, pad]
5043
4483
  // with the mode u32 occupying the third 4-byte slot via tonemapToU32 (SSOT in
5044
4484
  // camera.ts). Run AFTER the user-entity collection above so the engine's
5045
4485
  // built-in provider is authoritative for its own reserved key (a user entity
5046
- // can never shadow `'forgeax::tonemap'`). The single active camera mirrors
4486
+ // can never shadow the Standard tonemap identity. The single active camera mirrors
5047
4487
  // recordFrame's `activeCameras[0]` selection.
5048
4488
  const tonemapCamera = cameras[0];
5049
4489
  if (tonemapCamera !== undefined) {
5050
- postProcessParams.set('forgeax::tonemap', tonemapParams(tonemapCamera));
4490
+ postProcessParams.set(STANDARD_TONEMAP_FEATURE_ID, tonemapParams(tonemapCamera));
5051
4491
  }
5052
4492
 
5053
4493
  return {
@@ -5055,11 +4495,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5055
4495
  lights,
5056
4496
  renderables,
5057
4497
  dispatch,
5058
- candidateRenderables,
5059
- candidateDispatch: sortedCandidateDispatch,
5060
- candidateDispatchMembership,
5061
- candidateDomain,
5062
- environment,
4498
+ skylight,
4499
+ skylightCount,
4500
+ skybox,
4501
+ skyboxCount,
5063
4502
  frustumStats: { culled: frustumCulled, total: frustumTotal },
5064
4503
  visibilityStats: { explicitlyHidden: explicitlyHidden.size },
5065
4504
  postProcessParams,
@@ -5069,135 +4508,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5069
4508
  };
5070
4509
  }
5071
4510
 
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
4511
  export function defaultMaterialSnapshot(materialHandle = 0): MaterialSnapshot {
5182
4512
  // Mid-grey unlit fallback (D-Q7 case B + extract-stage missing-spec),
5183
4513
  // matching the pre-w6 visual outcome where the record stage's force-cast
5184
4514
  // read of `firstMaterial.baseColorTexture` returned undefined and the
5185
4515
  // 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
4516
  return {
5195
4517
  baseColor: vec3.create(0.5, 0.5, 0.5),
5196
4518
  metallic: 0,
5197
4519
  roughness: 1,
5198
4520
  materialHandle,
5199
- shadowCaster: true,
5200
- materialParamSchema,
5201
- derivedMaterialSlot,
5202
4521
  };
5203
4522
  }