@forgeax/engine-render 0.1.23 → 0.1.25

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 (322) hide show
  1. package/README.md +231 -10
  2. package/dist/assembly/bundler-contract.d.ts +9 -0
  3. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +8 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  7. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +7 -243
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +3 -2
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  13. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  14. package/dist/assembly/material/surface-projection.d.ts +24 -0
  15. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-policy.d.ts +188 -0
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  18. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  19. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  20. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  21. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  23. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  24. package/dist/assembly/recovery/generation.d.ts +46 -0
  25. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  26. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  27. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  28. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  29. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  30. package/dist/assembly/renderer-facade.d.ts +10 -0
  31. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +19 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  36. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  37. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  41. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-ready.d.ts +55 -0
  43. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  45. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  47. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  48. package/dist/assets/asset-decoders.d.ts.map +1 -1
  49. package/dist/authoring.mjs +1 -1
  50. package/dist/{chunk-FF7K3FSO.mjs → chunk-ADHHVYLW.mjs} +89 -89
  51. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  52. package/dist/{chunk-YALPS44A.mjs → chunk-E34VL5VI.mjs} +47 -47
  53. package/dist/chunk-E34VL5VI.mjs.map +1 -0
  54. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  55. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  56. package/dist/{chunk-HZUPSYW7.mjs → chunk-HKXTW355.mjs} +105 -15
  57. package/dist/chunk-HKXTW355.mjs.map +1 -0
  58. package/dist/chunk-JSEDTZYS.mjs +413 -0
  59. package/dist/chunk-JSEDTZYS.mjs.map +1 -0
  60. package/dist/{chunk-FBC4R44J.mjs → chunk-KM2NOX2I.mjs} +57 -57
  61. package/dist/chunk-KM2NOX2I.mjs.map +1 -0
  62. package/dist/{chunk-DIYW3M5D.mjs → chunk-N3RT2EUR.mjs} +2288 -2238
  63. package/dist/chunk-N3RT2EUR.mjs.map +1 -0
  64. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-TCLDX7SZ.mjs} +1167 -368
  65. package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
  66. package/dist/{chunk-QODA5UKU.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  67. package/dist/chunk-ZZ4YQ474.mjs.map +1 -0
  68. package/dist/components/instances.d.ts +13 -31
  69. package/dist/components/instances.d.ts.map +1 -1
  70. package/dist/construct-renderer.mjs +20688 -16314
  71. package/dist/construct-renderer.mjs.map +1 -1
  72. package/dist/device/device-scope.d.ts +2 -0
  73. package/dist/device/device-scope.d.ts.map +1 -1
  74. package/dist/device/gpu-residency.d.ts +24 -4
  75. package/dist/device/gpu-residency.d.ts.map +1 -1
  76. package/dist/environment/frame.d.ts +5 -0
  77. package/dist/environment/frame.d.ts.map +1 -1
  78. package/dist/environment/inspection.d.ts +7 -0
  79. package/dist/environment/inspection.d.ts.map +1 -1
  80. package/dist/environment/lifecycle.d.ts +20 -2
  81. package/dist/environment/lifecycle.d.ts.map +1 -1
  82. package/dist/errors/recover.d.ts +58 -2
  83. package/dist/errors/recover.d.ts.map +1 -1
  84. package/dist/errors/render.d.ts +13 -0
  85. package/dist/errors/render.d.ts.map +1 -1
  86. package/dist/extract/contracts.d.ts +899 -0
  87. package/dist/extract/contracts.d.ts.map +1 -0
  88. package/dist/features/host.d.ts +2 -0
  89. package/dist/features/host.d.ts.map +1 -1
  90. package/dist/features/prepared-gpu-work.d.ts +3 -0
  91. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  92. package/dist/features/prepared-graphics-store.d.ts +2 -0
  93. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  94. package/dist/gpu-driven/production-raster.d.ts +34 -1
  95. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +5 -0
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  99. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  100. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  101. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  102. package/dist/index.d.ts +2 -1
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.mjs +27 -9
  105. package/dist/index.mjs.map +1 -1
  106. package/dist/instance-buffer-cache.d.ts +7 -1
  107. package/dist/instance-buffer-cache.d.ts.map +1 -1
  108. package/dist/instances.d.ts +101 -0
  109. package/dist/instances.d.ts.map +1 -0
  110. package/dist/internal.mjs +6 -6
  111. package/dist/light-buffer-layout.d.ts +3 -0
  112. package/dist/light-buffer-layout.d.ts.map +1 -1
  113. package/dist/materials.d.ts +96 -11
  114. package/dist/materials.d.ts.map +1 -1
  115. package/dist/mesh-material-bindings.d.ts +56 -0
  116. package/dist/mesh-material-bindings.d.ts.map +1 -1
  117. package/dist/occlusion-proxy.d.ts +4 -0
  118. package/dist/occlusion-proxy.d.ts.map +1 -0
  119. package/dist/pbr-pipeline.d.ts +32 -9
  120. package/dist/pbr-pipeline.d.ts.map +1 -1
  121. package/dist/pipeline-builder.d.ts.map +1 -1
  122. package/dist/pipeline-spec.d.ts.map +1 -1
  123. package/dist/points-lines/prepare.d.ts +3 -0
  124. package/dist/points-lines/prepare.d.ts.map +1 -1
  125. package/dist/points-lines/standard-owner.d.ts +33 -0
  126. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  127. package/dist/record/frame-snapshot.d.ts +18 -2
  128. package/dist/record/frame-snapshot.d.ts.map +1 -1
  129. package/dist/record/frame-targets.d.ts.map +1 -1
  130. package/dist/record/frame.d.ts +14 -2
  131. package/dist/record/frame.d.ts.map +1 -1
  132. package/dist/record/main-pass-geometry.d.ts +36 -4
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +7 -17
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass.d.ts.map +1 -1
  137. package/dist/record/mesh-ssbo.d.ts +11 -3
  138. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  139. package/dist/record/recovery-pipeline.d.ts +30 -0
  140. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  141. package/dist/record/render-context.d.ts +28 -5
  142. package/dist/record/render-context.d.ts.map +1 -1
  143. package/dist/record/shadow-pass.d.ts +11 -1
  144. package/dist/record/shadow-pass.d.ts.map +1 -1
  145. package/dist/record/typed-frame-graph.d.ts +1 -1
  146. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  147. package/dist/recovery/render-system-candidate.d.ts +65 -0
  148. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  149. package/dist/render-contract.d.ts +68 -7
  150. package/dist/render-contract.d.ts.map +1 -1
  151. package/dist/render-system-extract-tail.d.ts +16 -0
  152. package/dist/render-system-extract-tail.d.ts.map +1 -0
  153. package/dist/render-system-extract.d.ts +180 -22
  154. package/dist/render-system-extract.d.ts.map +1 -1
  155. package/dist/render-system.d.ts +131 -4
  156. package/dist/render-system.d.ts.map +1 -1
  157. package/dist/scene/render-scene-types.d.ts +2 -0
  158. package/dist/scene/render-scene-types.d.ts.map +1 -1
  159. package/dist/scene/render-scene.d.ts +31 -1
  160. package/dist/scene/render-scene.d.ts.map +1 -1
  161. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  162. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  163. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  164. package/dist/temporal/index.mjs +6 -6
  165. package/dist/temporal/taa-history-store.d.ts +15 -1
  166. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  167. package/dist/temporal/transaction.d.ts +5 -0
  168. package/dist/temporal/transaction.d.ts.map +1 -1
  169. package/package.json +20 -20
  170. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  171. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  172. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  173. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  174. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  175. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  176. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  177. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  178. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  179. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  180. package/src/__tests__/instances-store.unit.test.ts +46 -0
  181. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  182. package/src/__tests__/material-contract-inventory.unit.test.ts +29 -22
  183. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  184. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  185. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  186. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  187. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  188. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  189. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  190. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  191. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -5
  192. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  193. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  194. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  195. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  196. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  197. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +127 -18
  198. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  199. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  200. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  201. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  202. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  203. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  204. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  205. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  206. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  207. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  208. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  209. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  210. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  211. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  212. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  213. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  214. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  215. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  216. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  217. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  218. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  219. package/src/assembly/bundler-contract.ts +9 -0
  220. package/src/assembly/device-feature-admission.ts +15 -5
  221. package/src/assembly/extended-lighting-fallback.ts +216 -0
  222. package/src/assembly/factory.ts +83 -10088
  223. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  224. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  225. package/src/assembly/material/assembly.ts +18 -2
  226. package/src/assembly/material/standard-layer-projection.ts +16 -0
  227. package/src/assembly/material/surface-projection.ts +90 -0
  228. package/src/assembly/material-shader-policy.ts +932 -0
  229. package/src/assembly/material-shader-prewarm.ts +88 -0
  230. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  231. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  232. package/src/assembly/recovery/generation.ts +141 -0
  233. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  234. package/src/assembly/recovery/renderer-recover.ts +906 -0
  235. package/src/assembly/renderer-facade.ts +342 -0
  236. package/src/assembly/renderer-frame-transaction.ts +45 -0
  237. package/src/assembly/renderer-helpers.ts +99 -0
  238. package/src/assembly/renderer-lifecycle.ts +159 -0
  239. package/src/assembly/shader-prewarm-policy.ts +157 -0
  240. package/src/assembly/webgpu-ready-contract.ts +14 -0
  241. package/src/assembly/webgpu-ready.ts +4066 -0
  242. package/src/assembly/webgpu-renderer.ts +4027 -0
  243. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  244. package/src/assets/asset-decoders.ts +27 -4
  245. package/src/components/camera.ts +1 -1
  246. package/src/components/instances.ts +14 -197
  247. package/src/debug-draw-glue.ts +1 -1
  248. package/src/device/device-scope.ts +35 -6
  249. package/src/device/gpu-residency.ts +194 -26
  250. package/src/environment/frame.ts +6 -0
  251. package/src/environment/inspection.ts +10 -0
  252. package/src/environment/lifecycle.ts +114 -8
  253. package/src/errors/recover.ts +76 -5
  254. package/src/errors/render.ts +17 -0
  255. package/src/extract/contracts.ts +974 -0
  256. package/src/features/host.ts +16 -0
  257. package/src/features/prepared-gpu-work.ts +20 -2
  258. package/src/features/prepared-graphics-store.ts +17 -0
  259. package/src/gpu-driven/production-raster.ts +131 -3
  260. package/src/gpu-scene.ts +87 -31
  261. package/src/ibl/IblPipelineCache.ts +134 -50
  262. package/src/ibl/skylight-bind-group.ts +26 -0
  263. package/src/index.ts +10 -1
  264. package/src/instance-buffer-cache.ts +14 -1
  265. package/src/instances.ts +288 -0
  266. package/src/light-buffer-layout.ts +3 -0
  267. package/src/materials.ts +365 -53
  268. package/src/mesh-material-bindings.ts +59 -0
  269. package/src/occlusion-proxy.ts +64 -0
  270. package/src/pbr-pipeline.ts +139 -15
  271. package/src/pipeline-builder.ts +14 -10
  272. package/src/pipeline-spec.ts +6 -14
  273. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  274. package/src/points-lines/prepare.ts +9 -0
  275. package/src/points-lines/standard-owner.ts +403 -0
  276. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  277. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  278. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  279. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  280. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  281. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  282. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  283. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  284. package/src/record/frame-snapshot.ts +34 -2
  285. package/src/record/frame-targets.ts +11 -1
  286. package/src/record/frame.ts +37 -4
  287. package/src/record/helpers.ts +1 -1
  288. package/src/record/main-pass-geometry.ts +727 -63
  289. package/src/record/main-pass-material.ts +316 -209
  290. package/src/record/main-pass-sprite-draws.ts +7 -8
  291. package/src/record/main-pass.ts +94 -6
  292. package/src/record/mesh-ssbo.ts +32 -10
  293. package/src/record/recovery-pipeline.ts +695 -0
  294. package/src/record/render-context.ts +39 -4
  295. package/src/record/shadow-pass.ts +46 -99
  296. package/src/record/typed-frame-graph.ts +45 -1
  297. package/src/record/view-ubo.ts +3 -3
  298. package/src/recovery/render-system-candidate.ts +876 -0
  299. package/src/render-contract.ts +69 -7
  300. package/src/render-system-extract-tail.ts +3039 -0
  301. package/src/render-system-extract.ts +177 -3003
  302. package/src/render-system.ts +906 -662
  303. package/src/scene/render-scene-types.ts +2 -0
  304. package/src/scene/render-scene.ts +703 -37
  305. package/src/systems/skin-palette-allocator.ts +129 -7
  306. package/src/temporal/frame-coordinator.ts +4 -1
  307. package/src/temporal/taa-history-store.ts +46 -2
  308. package/src/temporal/temporal-view.ts +1 -1
  309. package/src/temporal/transaction.ts +9 -0
  310. package/src/tilemap-chunk-extract-system.ts +1 -1
  311. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  312. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  313. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  314. package/dist/chunk-4NPUASZR.mjs +0 -203
  315. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  316. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  317. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  318. package/dist/chunk-FF7K3FSO.mjs.map +0 -1
  319. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  320. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  321. package/dist/chunk-YALPS44A.mjs.map +0 -1
  322. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -10,7 +10,7 @@ import {
10
10
  RhiError,
11
11
  type RhiRenderPassEncoder,
12
12
  } from '@forgeax/engine-rhi';
13
- import type { PassKind, PrimitiveTopology } from '@forgeax/engine-types';
13
+ import type { ParamSchemaEntry, PassKind, PrimitiveTopology } from '@forgeax/engine-types';
14
14
  import { GpuBuffer } from '../gpu-resource';
15
15
  import {
16
16
  GPU_BUFFER_USAGE_COPY_DST,
@@ -19,11 +19,20 @@ import {
19
19
  } from '../gpu-usage';
20
20
  import { createHdrpSkinUnifiedBindGroup, getOrCreateHdrpBuffers } from '../hdrp-buffers';
21
21
  import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
22
+ import {
23
+ type InstanceCollectionFailureCode,
24
+ recordInstanceFailure,
25
+ recordInstanceResidency,
26
+ } from '../instances';
22
27
  import { SPRITE_PREMULTIPLIED_ALPHA_BLEND } from '../materials';
23
28
  import {
24
29
  createPbrSkinMeshBindGroupEntries,
30
+ isCanonicalStandardPbrMaterialShader,
25
31
  isStandardPbrMaterialShader,
32
+ isStandardPbrSkinMaterialShader,
33
+ materialBindGroupLayoutIdentity,
26
34
  pbrSkinMeshDynamicOffsets,
35
+ physicalTextureFields,
27
36
  SKIN_MATERIAL_SHADER_ID,
28
37
  } from '../pbr-pipeline';
29
38
  import {
@@ -35,13 +44,17 @@ import {
35
44
  import type { PointsLinesRecordPlan } from '../points-lines/record';
36
45
  import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
37
46
  import type { RenderRecordPhase } from '../render-contract';
38
- import type { MaterialSnapshot } from '../render-system-extract';
47
+ import type { MaterialSnapshot, RenderableSnapshot } from '../render-system-extract';
39
48
  import {
40
49
  PROBE_BLEND_RECORD_BYTE_SIZE,
41
50
  PROBE_BLEND_RECORD_STRIDE,
42
51
  probeBlendRecordOffset,
43
52
  } from '../scene/probe-blend-record';
44
- import { worldEntityKey } from './frame-snapshot';
53
+ import {
54
+ getOpaqueResourceIdentity,
55
+ instanceCollectionCacheKey,
56
+ worldEntityKey,
57
+ } from './frame-snapshot';
45
58
  import {
46
59
  geometryRenderStateForTopology,
47
60
  isEntityFullyTransparent,
@@ -50,6 +63,7 @@ import {
50
63
  } from './main-pass-material';
51
64
  import {
52
65
  getOrCreateFromChain,
66
+ INSTANCE_STORAGE_STRIDE_FLOATS,
53
67
  INSTANCE_UBO_FULL_ARRAY_BYTES,
54
68
  MAX_UNIFORM_INSTANCES,
55
69
  MESH_PER_ENTITY_STRIDE,
@@ -69,12 +83,36 @@ type GeometryInstanceDraw = {
69
83
  readonly instanceCount: number;
70
84
  };
71
85
 
86
+ type InstanceUploadResult =
87
+ | {
88
+ readonly ok: true;
89
+ readonly ranges: readonly { readonly start: number; readonly end: number }[];
90
+ readonly bytes: number;
91
+ }
92
+ | {
93
+ readonly ok: false;
94
+ readonly ranges: readonly { readonly start: number; readonly end: number }[];
95
+ readonly bytes: number;
96
+ };
97
+
72
98
  type GeometryBindingState = {
73
99
  pipeline: RenderPipeline | null;
74
100
  instancesBuffer: Buffer | null;
75
101
  probeOffset: number;
76
102
  };
77
103
 
104
+ function materialDiagnosticsEnabled(): boolean {
105
+ if (typeof globalThis !== 'object' || globalThis === null || !('process' in globalThis)) {
106
+ return false;
107
+ }
108
+ const processValue = (
109
+ globalThis as {
110
+ readonly process?: { readonly env?: Record<string, string | undefined> };
111
+ }
112
+ ).process;
113
+ return processValue?.env?.FORGEAX_MATERIAL_DIAGNOSTICS === '1';
114
+ }
115
+
78
116
  type MaterialPipelineLookup = {
79
117
  readonly materialShaderId: string;
80
118
  readonly isHdrTarget: boolean;
@@ -94,6 +132,121 @@ type GeometryRecordProfileSegment = 'material-bind-groups' | 'pipeline-selection
94
132
 
95
133
  type GeometryRenderState = ReturnType<typeof geometryRenderStateForTopology>;
96
134
 
135
+ /**
136
+ * Select the transmission shader axis from authored material data, not the
137
+ * canonical boot schema. The latter reserves transmission fields for the
138
+ * shared Standard ABI even when a material remains base-only.
139
+ */
140
+ export function requestsStandardTransmissionVariant(
141
+ material: Pick<MaterialSnapshot, 'materialShaderId' | 'paramSnapshot'>,
142
+ ): boolean {
143
+ return (
144
+ material.materialShaderId === 'forgeax::default-standard-pbr' &&
145
+ material.paramSnapshot?.transmission !== undefined
146
+ );
147
+ }
148
+
149
+ function recordIblPipelineBinding(
150
+ c: _InternalRenderPipelineContext,
151
+ material: MaterialSnapshot,
152
+ pipeline: RenderPipeline,
153
+ materialGroup: BindGroup,
154
+ ): void {
155
+ const receipt = c.frameState.iblBindingInspection;
156
+ if (receipt === undefined || !isStandardPbrMaterialShader(material.materialShaderId)) return;
157
+ const materialBgl =
158
+ c.runtime.getMaterialBindGroupLayout?.(
159
+ material.materialShaderId ?? 'forgeax::default-standard-pbr',
160
+ ) ?? c.pipelineState.materialBindGroupLayout;
161
+ c.frameState.iblBindingInspection = {
162
+ ...receipt,
163
+ pipeline: {
164
+ pipelineIdentity: getOpaqueResourceIdentity(pipeline as object),
165
+ effectiveMaterialLayoutIdentity: effectiveMaterialLayoutIdentity(
166
+ material.materialShaderId ?? 'forgeax::default-standard-pbr',
167
+ material.materialParamSchema,
168
+ ),
169
+ materialBglIdentity: getOpaqueResourceIdentity(materialBgl as object),
170
+ bindGroupIdentity: getOpaqueResourceIdentity(materialGroup as object),
171
+ drawFrameId: c.frameState.frameNumber,
172
+ },
173
+ };
174
+ const materialReceipt = c.frameState.iblBindingInspection.material;
175
+ const errors: string[] = [];
176
+ if (
177
+ materialReceipt !== undefined &&
178
+ materialReceipt.materialBglIdentity !== getOpaqueResourceIdentity(materialBgl as object)
179
+ ) {
180
+ errors.push('material-pipeline-bgl-identity-mismatch');
181
+ }
182
+ if (
183
+ materialReceipt !== undefined &&
184
+ materialReceipt.bindGroupIdentity !== getOpaqueResourceIdentity(materialGroup as object)
185
+ ) {
186
+ errors.push('material-bind-group-identity-mismatch');
187
+ }
188
+ if (materialReceipt !== undefined) {
189
+ const expectedResourceIdentities = [
190
+ receipt.resources.irradiance.viewIdentity,
191
+ receipt.resources.irradiance.samplerIdentity,
192
+ receipt.resources.prefilter.viewIdentity,
193
+ receipt.resources.prefilter.samplerIdentity,
194
+ receipt.resources.brdfLut.viewIdentity,
195
+ receipt.resources.brdfLut.samplerIdentity,
196
+ receipt.resources.intensityBufferIdentity,
197
+ ];
198
+ const actualResourceIdentities = materialReceipt.entries
199
+ .filter((entry) => entry.binding >= materialReceipt.skylightBindingStart)
200
+ .slice(0, expectedResourceIdentities.length)
201
+ .map((entry) => entry.resourceIdentity);
202
+ if (
203
+ actualResourceIdentities.length !== expectedResourceIdentities.length ||
204
+ actualResourceIdentities.some(
205
+ (identity, index) => identity !== expectedResourceIdentities[index],
206
+ )
207
+ ) {
208
+ errors.push('material-ibl-resource-identity-mismatch');
209
+ }
210
+ }
211
+ c.frameState.iblBindingInspection = {
212
+ ...c.frameState.iblBindingInspection,
213
+ status: errors.length === 0 ? 'binding-chain-consistent' : 'binding-chain-mismatch',
214
+ errors,
215
+ };
216
+ }
217
+
218
+ const SHARED_BOOT_MATERIAL_SHADER_IDS = new Set([
219
+ 'forgeax::default-unlit',
220
+ 'forgeax::default-shadow-caster',
221
+ 'forgeax::sprite',
222
+ 'forgeax::sprite-lit',
223
+ 'forgeax::default-sprite',
224
+ 'forgeax::msdf-text',
225
+ 'forgeax::default-standard-pbr',
226
+ 'forgeax::pbr-skin',
227
+ 'forgeax::default-standard-pbr-skin',
228
+ ]);
229
+
230
+ /**
231
+ * Returns the layout identity that can distinguish a material PSO in the
232
+ * record cache. Shared boot shaders keep the boot layout identity even when a
233
+ * manifest exposes a schema; Standard PBR only opts into a schema-derived
234
+ * identity when authored clearcoat texture bindings extend that layout.
235
+ */
236
+ export function effectiveMaterialLayoutIdentity(
237
+ materialShaderId: string,
238
+ materialParamSchema?: readonly ParamSchemaEntry[],
239
+ ): string | undefined {
240
+ if (materialParamSchema === undefined) return undefined;
241
+ const hasStandardPhysicalMaps =
242
+ isStandardPbrMaterialShader(materialShaderId) &&
243
+ physicalTextureFields(materialParamSchema).length > 0;
244
+ if (SHARED_BOOT_MATERIAL_SHADER_IDS.has(materialShaderId) && !hasStandardPhysicalMaps) {
245
+ return undefined;
246
+ }
247
+ return materialBindGroupLayoutIdentity(materialShaderId, materialParamSchema);
248
+ }
249
+
97
250
  /**
98
251
  * Resolve the depth contract for a geometry pass without changing the base
99
252
  * material state. Temporal scene-data shares the main pass depth attachment:
@@ -228,6 +381,7 @@ export function recordGeometryDraws(
228
381
  viewBindGroup,
229
382
  viewBindGroupDynamicOffset = 0,
230
383
  } = c;
384
+ const diagnosticsEnabled = materialDiagnosticsEnabled();
231
385
  const colorFormatOverride = c.transparentColorFormat as GPUTextureFormat | undefined;
232
386
  // The prepared Standard topology is the frame authority. A bind-group
233
387
  // identity is only a resource result; using it to infer the shader lane
@@ -587,7 +741,22 @@ export function recordGeometryDraws(
587
741
  // and walks 0, 1536, 3072, ... on the storage path.
588
742
  const skinAllocator = pipelineState.skinPaletteAllocator;
589
743
  const skinSlice = entry.source.skin;
590
- const clusteredSkin = clusteredLighting;
744
+ // Authored Standard skin aliases deliberately publish the physical root
745
+ // with only their STORAGE_BUFFER capability axis. Their composed WGSL is
746
+ // the direct-lighting/palette (`skin`) ABI, even when the frame's
747
+ // canonical Standard lane uses clustered lighting. Only the canonical
748
+ // engine skin artifact can consume the HDRP unified skin group(2) layout;
749
+ // selecting it from the frame topology alone would bind
750
+ // `hdrp-skin-unified-bg-group2` to a three-entry URP skin PSO.
751
+ const clusteredSkin =
752
+ clusteredLighting &&
753
+ entry.source.materials.every((material) => {
754
+ const shaderId = material.materialShaderId;
755
+ return (
756
+ !isStandardPbrSkinMaterialShader(shaderId) ||
757
+ isCanonicalStandardPbrMaterialShader(shaderId)
758
+ );
759
+ });
591
760
  const skinMeshBindGroupLayout = clusteredSkin
592
761
  ? pipelineState.hdrpSkinMeshBindGroupLayout
593
762
  : pipelineState.pbrSkinMeshBindGroupLayout;
@@ -734,6 +903,19 @@ export function recordGeometryDraws(
734
903
  // the async PSO compile resolves the cache hits and the next frame
735
904
  // routes the skin BG + skin pipeline together. Mirrors the uniform
736
905
  // null skip-draw shape (M6-T1, charter P3 explicit failure).
906
+ if (diagnosticsEnabled) {
907
+ console.error(
908
+ `[render-material] skin draw skipped: ${JSON.stringify({
909
+ entityKey: entry.source.entityKey,
910
+ materialHandle: entry.source.material.materialHandle,
911
+ reason: 'skin-pipeline-unavailable',
912
+ skinResourcesReady: skinResources !== null,
913
+ skinPsoReady: skinPsoProbe !== null,
914
+ paletteBufferReady: skinSlice?.buffer !== undefined,
915
+ paletteByteOffset: skinSlice?.byteOffset,
916
+ })}`,
917
+ );
918
+ }
737
919
  continue;
738
920
  }
739
921
  // feat-20260520-2d-sprite-layer-mvp M-3 / w25 (@fallback sprite
@@ -828,6 +1010,26 @@ export function recordGeometryDraws(
828
1010
  // injection, and Skylight merge; deduped cross-entity via the
829
1011
  // shaderId-outer `materialBgShared` cache.
830
1012
  const materialSlot = materialSlotIndices[i]?.[matSlotIdx] ?? materialSlotIndices[i]?.[0] ?? 0;
1013
+ const processValue = (
1014
+ globalThis as {
1015
+ process?: { env?: Record<string, string | undefined> };
1016
+ }
1017
+ ).process;
1018
+ if (
1019
+ processValue?.env?.FORGEAX_MATERIAL_PIPELINE_DIAGNOSTICS === '1' &&
1020
+ (submeshMaterial.materialShaderId === 'forgeax::default-standard-pbr' ||
1021
+ submeshMaterial.materialShaderId === 'forgeax::pbr-skin')
1022
+ ) {
1023
+ console.error(
1024
+ `[render-material] draw receipt: ${JSON.stringify({
1025
+ entityIndex: entry.renderableIndex,
1026
+ materialSlot,
1027
+ materialHandle: submeshMaterial.materialHandle,
1028
+ clearcoat: submeshMaterial.paramSnapshot?.clearcoat,
1029
+ shader: submeshMaterial.materialShaderId,
1030
+ })}`,
1031
+ );
1032
+ }
831
1033
  perSubmeshBg =
832
1034
  profilePhase === undefined
833
1035
  ? resolveMaterialBindGroup(
@@ -849,7 +1051,9 @@ export function recordGeometryDraws(
849
1051
  const smTopology = sm.topology;
850
1052
  const smMaterialShaderId =
851
1053
  entry.source.skin !== undefined
852
- ? SKIN_MATERIAL_SHADER_ID
1054
+ ? isStandardPbrSkinMaterialShader(submeshMaterial.materialShaderId)
1055
+ ? submeshMaterial.materialShaderId
1056
+ : SKIN_MATERIAL_SHADER_ID
853
1057
  : submeshMaterial.materialShaderId;
854
1058
  const probeBlendAvailable =
855
1059
  probeBlendRecordAvailable && isStandardPbrMaterialShader(smMaterialShaderId);
@@ -930,6 +1134,11 @@ export function recordGeometryDraws(
930
1134
  hasVertexColor,
931
1135
  probeBlendAvailable,
932
1136
  );
1137
+ const transmissionAvailable = requestsStandardTransmissionVariant(submeshMaterial);
1138
+ const materialCapabilityVariantSet =
1139
+ transmissionAvailable && !capabilityVariantSet.includes('TRANSMISSION_AVAILABLE=')
1140
+ ? `${capabilityVariantSet === '' ? '' : `${capabilityVariantSet}+`}TRANSMISSION_AVAILABLE=true`
1141
+ : capabilityVariantSet;
933
1142
  // Sprite's base request must retain the boot-selected artifact. Only
934
1143
  // SpriteInstances uses the explicit empty key for the
935
1144
  // PER_INSTANCE_REGION=true variant; synthesizing the generic PBR
@@ -940,7 +1149,7 @@ export function recordGeometryDraws(
940
1149
  ? ''
941
1150
  : undefined
942
1151
  : entry.variantSet === undefined
943
- ? capabilityVariantSet
1152
+ ? materialCapabilityVariantSet
944
1153
  : capabilityVariantSet === ''
945
1154
  ? entry.variantSet
946
1155
  : `${capabilityVariantSet}+${entry.variantSet}`;
@@ -973,6 +1182,16 @@ export function recordGeometryDraws(
973
1182
  }
974
1183
 
975
1184
  if (smPipelineHandle === null) {
1185
+ if (diagnosticsEnabled) {
1186
+ console.error(
1187
+ `[render-material] draw skipped: ${JSON.stringify({
1188
+ entityKey: entry.source.entityKey,
1189
+ materialHandle: submeshMaterial.materialHandle,
1190
+ shader: smMaterialShaderId,
1191
+ reason: 'pipeline-unavailable',
1192
+ })}`,
1193
+ );
1194
+ }
976
1195
  continue;
977
1196
  }
978
1197
  // The probe record is an ABI extension of the built-in Standard PBR
@@ -1003,6 +1222,31 @@ export function recordGeometryDraws(
1003
1222
  if (selectedGroup === null) continue;
1004
1223
  group2BindGroup = selectedGroup;
1005
1224
  }
1225
+ if (diagnosticsEnabled && submeshMaterial.textureHandles !== undefined) {
1226
+ console.error(
1227
+ `[render-material] draw submitted: ${JSON.stringify({
1228
+ entityKey: entry.source.entityKey,
1229
+ materialHandle: submeshMaterial.materialHandle,
1230
+ shader: smMaterialShaderId,
1231
+ textureHandles: [...submeshMaterial.textureHandles.entries()].map(
1232
+ ([field, handle]) => ({
1233
+ field,
1234
+ handle,
1235
+ }),
1236
+ ),
1237
+ skin: isSkinEntry
1238
+ ? {
1239
+ resourcesReady: skinResources !== null,
1240
+ psoReady: skinPsoProbe !== null,
1241
+ paletteBufferReady: skinSlice?.buffer !== undefined,
1242
+ paletteByteOffset: skinSlice?.byteOffset,
1243
+ dynamicOffsets: [...group2DynamicOffsets],
1244
+ }
1245
+ : undefined,
1246
+ pipelineReady: true,
1247
+ })}`,
1248
+ );
1249
+ }
1006
1250
  pass.setBindGroup(2, group2BindGroup, group2DynamicOffsets);
1007
1251
 
1008
1252
  if (profilePhase === undefined) {
@@ -1034,6 +1278,9 @@ export function recordGeometryDraws(
1034
1278
  ),
1035
1279
  );
1036
1280
  }
1281
+ if (diagnosticsEnabled && perSubmeshBg !== null) {
1282
+ recordIblPipelineBinding(c, submeshMaterial, smPipelineHandle, perSubmeshBg);
1283
+ }
1037
1284
  }
1038
1285
  }
1039
1286
  }
@@ -1043,8 +1290,285 @@ export function recordGeometryDraws(
1043
1290
  *
1044
1291
  * @internal
1045
1292
  */
1046
- function resolveGeometryInstanceBuffer(
1047
- c: _InternalRenderPipelineContext,
1293
+ function uploadInstanceRanges(
1294
+ c: Pick<
1295
+ _InternalRenderPipelineContext,
1296
+ 'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
1297
+ >,
1298
+ inst: NonNullable<RenderableSnapshot['instances']>,
1299
+ buffer: Buffer,
1300
+ ranges: readonly { readonly start: number; readonly end: number }[],
1301
+ storageBacked: boolean,
1302
+ sourceOffsetInstances = 0,
1303
+ ): InstanceUploadResult {
1304
+ const strideBytes =
1305
+ (storageBacked ? INSTANCE_STORAGE_STRIDE_FLOATS : 16) * Float32Array.BYTES_PER_ELEMENT;
1306
+ const uploadedRanges: { start: number; end: number }[] = [];
1307
+ let uploadedBytes = 0;
1308
+ for (const range of ranges) {
1309
+ const start = Math.max(0, Math.min(inst.instanceCount, range.start));
1310
+ const end = Math.max(start, Math.min(inst.instanceCount, range.end));
1311
+ if (end <= start) continue;
1312
+ const source = inst.transforms.subarray(
1313
+ (sourceOffsetInstances + start) * 16,
1314
+ (sourceOffsetInstances + end) * 16,
1315
+ );
1316
+ const payload = storageBacked ? packInstanceStorageBuffer(source) : source;
1317
+ const written = c.runtime.device.queue.writeBuffer(buffer, start * strideBytes, payload);
1318
+ if (!written.ok) {
1319
+ c.runtime.errorRegistry.fire(written.error);
1320
+ return { ok: false, ranges: uploadedRanges, bytes: uploadedBytes };
1321
+ }
1322
+ uploadedRanges.push({ start, end });
1323
+ uploadedBytes += payload.byteLength;
1324
+ }
1325
+ return { ok: true, ranges: uploadedRanges, bytes: uploadedBytes };
1326
+ }
1327
+
1328
+ export function reportInstanceResidency(
1329
+ c: Pick<
1330
+ _InternalRenderPipelineContext,
1331
+ 'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
1332
+ >,
1333
+ inst: NonNullable<RenderableSnapshot['instances']>,
1334
+ input: {
1335
+ readonly lane: 'direct-storage' | 'chunked-storage' | 'direct-uniform' | 'chunked-uniform';
1336
+ readonly requestedBytes: number;
1337
+ readonly supportedBytes: number | undefined;
1338
+ readonly uploadRanges: readonly { readonly start: number; readonly end: number }[];
1339
+ readonly uploadedBytes: number;
1340
+ },
1341
+ ): void {
1342
+ if (inst.collectionId === undefined || c.frameState.instanceResidency === undefined) return;
1343
+ recordInstanceResidency(c.frameState.instanceResidency, {
1344
+ collectionId: inst.collectionId,
1345
+ frameNumber: c.frameState.frameNumber,
1346
+ residentGeneration: c.runtime.deviceScope.generation,
1347
+ lane: input.lane,
1348
+ requestedBytes: input.requestedBytes,
1349
+ supportedBytes: input.supportedBytes,
1350
+ uploadRanges: input.uploadRanges,
1351
+ uploadedBytes: input.uploadedBytes,
1352
+ backend: c.runtime.device.caps.backendKind,
1353
+ });
1354
+ }
1355
+
1356
+ function reportInstanceFailure(
1357
+ c: Pick<
1358
+ _InternalRenderPipelineContext,
1359
+ 'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
1360
+ >,
1361
+ inst: NonNullable<RenderableSnapshot['instances']>,
1362
+ error: {
1363
+ readonly code: InstanceCollectionFailureCode;
1364
+ readonly expected: string;
1365
+ readonly hint: string;
1366
+ },
1367
+ requestedBytes: number,
1368
+ supportedBytes: number | undefined,
1369
+ ): void {
1370
+ if (inst.collectionId === undefined || c.frameState.instanceResidency === undefined) return;
1371
+ recordInstanceFailure(c.frameState.instanceResidency, {
1372
+ collectionId: inst.collectionId,
1373
+ code: error.code,
1374
+ expected: error.expected,
1375
+ hint: error.hint,
1376
+ facts: {
1377
+ requestedBytes,
1378
+ supportedBytes,
1379
+ backend: c.runtime.device.caps.backendKind,
1380
+ owner: 'renderer.instances',
1381
+ cause: error.code,
1382
+ recovery: error.hint,
1383
+ },
1384
+ });
1385
+ }
1386
+
1387
+ function clearInstanceChunksForOwner(
1388
+ c: Pick<
1389
+ _InternalRenderPipelineContext,
1390
+ 'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
1391
+ >,
1392
+ ownerKey: number,
1393
+ ): void {
1394
+ const chunks = c.frameState.instanceBufferChunks;
1395
+ if (chunks === undefined) return;
1396
+ const prefix = `${ownerKey}:`;
1397
+ for (const [key, entry] of chunks.entries()) {
1398
+ if (!key.startsWith(prefix)) continue;
1399
+ if (!entry.buffer.isDestroyed) {
1400
+ const destroyed = entry.buffer.destroy();
1401
+ if (!destroyed.ok) c.runtime.errorRegistry.fire(destroyed.error);
1402
+ }
1403
+ chunks.delete(key);
1404
+ }
1405
+ }
1406
+
1407
+ export function resolveStorageInstanceChunks(
1408
+ c: Pick<
1409
+ _InternalRenderPipelineContext,
1410
+ 'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
1411
+ >,
1412
+ entry: _InternalRenderPipelineContext['validatedOrdered'][number],
1413
+ inst: NonNullable<RenderableSnapshot['instances']>,
1414
+ cap: number,
1415
+ probeBuffer: Buffer | undefined,
1416
+ probeOffset: number,
1417
+ ): readonly GeometryInstanceDraw[] | null {
1418
+ const { runtime, frameState } = c;
1419
+ const strideBytes = INSTANCE_STORAGE_STRIDE_FLOATS * Float32Array.BYTES_PER_ELEMENT;
1420
+ const chunkCapacity = Math.floor(cap / strideBytes);
1421
+ if (chunkCapacity < 1) {
1422
+ reportInstanceFailure(
1423
+ c,
1424
+ inst,
1425
+ {
1426
+ code: 'limit-exceeded',
1427
+ expected: `maxStorageBufferBindingSize (${cap}) >= ${strideBytes}`,
1428
+ hint: 'use a backend with storage-buffer bindings large enough for one InstanceData record',
1429
+ },
1430
+ inst.instanceCount * strideBytes,
1431
+ cap,
1432
+ );
1433
+ runtime.errorRegistry.fire(
1434
+ new RhiError({
1435
+ code: 'limit-exceeded',
1436
+ expected: `maxStorageBufferBindingSize (${cap}) >= ${strideBytes}`,
1437
+ hint: 'use a backend with storage-buffer bindings large enough for one InstanceData record',
1438
+ detail: { maxStorageBufferBindingSize: cap, requestedBytes: strideBytes },
1439
+ }),
1440
+ );
1441
+ return null;
1442
+ }
1443
+ const ownerKey = instanceCollectionCacheKey(entry.source.worldId, inst);
1444
+ const chunks = frameState.instanceBufferChunks;
1445
+ const activeKeys = new Set<string>();
1446
+ const draws: GeometryInstanceDraw[] = [];
1447
+ const uploadedRanges: { start: number; end: number }[] = [];
1448
+ let uploadedBytes = 0;
1449
+ for (let start = 0; start < inst.instanceCount; start += chunkCapacity) {
1450
+ const end = Math.min(inst.instanceCount, start + chunkCapacity);
1451
+ const count = end - start;
1452
+ const chunkKey = `${ownerKey}:${start}`;
1453
+ activeKeys.add(chunkKey);
1454
+ // Keep stable-resident checks arithmetic-only; packing is deferred to the
1455
+ // upload path below, where a new or dirty range actually needs payload.
1456
+ const payloadBytes = count * strideBytes;
1457
+ const previous = chunks?.get(chunkKey);
1458
+ let active =
1459
+ previous !== undefined &&
1460
+ previous.uploadedArchVersion === inst.archVersion &&
1461
+ previous.uploadedByteLength === payloadBytes
1462
+ ? previous
1463
+ : undefined;
1464
+ let activeIsNew = false;
1465
+ if (active === undefined) {
1466
+ const created = runtime.device.createBuffer({
1467
+ size: payloadBytes,
1468
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
1469
+ mappedAtCreation: false,
1470
+ });
1471
+ if (!created.ok) {
1472
+ runtime.errorRegistry.fire(created.error);
1473
+ reportInstanceFailure(c, inst, created.error, inst.instanceCount * strideBytes, cap);
1474
+ return null;
1475
+ }
1476
+ if (previous !== undefined && !previous.buffer.isDestroyed) {
1477
+ const destroyed = previous.buffer.destroy();
1478
+ if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
1479
+ }
1480
+ active = {
1481
+ buffer: new GpuBuffer(runtime.device, created.value),
1482
+ uploadedArchVersion: inst.archVersion,
1483
+ uploadedByteLength: payloadBytes,
1484
+ };
1485
+ activeIsNew = true;
1486
+ chunks?.set(chunkKey, active);
1487
+ if (chunks === undefined) frameState.transientInstanceBuffers.push(active);
1488
+ }
1489
+ if (active === undefined) return null;
1490
+ const needsUpload =
1491
+ activeIsNew || active.uploadedRevision !== (inst.revision ?? inst.archVersion);
1492
+ const rangesToUpload = [
1493
+ {
1494
+ start: 0,
1495
+ end: count,
1496
+ },
1497
+ ];
1498
+ if (needsUpload && rangesToUpload.length > 0) {
1499
+ const upload = uploadInstanceRanges(
1500
+ c,
1501
+ inst,
1502
+ active.buffer.handle,
1503
+ rangesToUpload,
1504
+ true,
1505
+ start,
1506
+ );
1507
+ for (const range of upload.ranges) {
1508
+ uploadedRanges.push({ start: range.start + start, end: range.end + start });
1509
+ }
1510
+ uploadedBytes += upload.bytes;
1511
+ if (!upload.ok) {
1512
+ reportInstanceFailure(
1513
+ c,
1514
+ inst,
1515
+ {
1516
+ code: 'queue-write-buffer-failed',
1517
+ expected:
1518
+ 'the renderer-owned instance storage buffer accepts the complete instance payload',
1519
+ hint: 'retry after the active device is healthy or recover the renderer',
1520
+ },
1521
+ inst.instanceCount * strideBytes,
1522
+ cap,
1523
+ );
1524
+ return null;
1525
+ }
1526
+ }
1527
+ if (needsUpload) {
1528
+ {
1529
+ const published = { ...active, uploadedRevision: inst.revision ?? inst.archVersion };
1530
+ active = published;
1531
+ chunks?.set(chunkKey, published);
1532
+ }
1533
+ }
1534
+ const activeBuffer = active;
1535
+ draws.push({
1536
+ instanceBuffer: activeBuffer.buffer.handle,
1537
+ instanceBindGroup: resolveGeometryInstancesBindGroup(
1538
+ c,
1539
+ activeBuffer.buffer.handle,
1540
+ probeBuffer,
1541
+ ),
1542
+ probeOffset,
1543
+ instanceCount: count,
1544
+ });
1545
+ }
1546
+ if (chunks !== undefined) {
1547
+ const prefix = `${ownerKey}:`;
1548
+ for (const [key, stale] of chunks.entries()) {
1549
+ if (!key.startsWith(prefix) || activeKeys.has(key)) continue;
1550
+ if (!stale.buffer.isDestroyed) {
1551
+ const destroyed = stale.buffer.destroy();
1552
+ if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
1553
+ }
1554
+ chunks.delete(key);
1555
+ }
1556
+ }
1557
+ reportInstanceResidency(c, inst, {
1558
+ lane: 'chunked-storage',
1559
+ requestedBytes: inst.instanceCount * strideBytes,
1560
+ supportedBytes: cap,
1561
+ uploadRanges: uploadedRanges,
1562
+ uploadedBytes,
1563
+ });
1564
+ return draws;
1565
+ }
1566
+
1567
+ export function resolveGeometryInstanceBuffer(
1568
+ c: Pick<
1569
+ _InternalRenderPipelineContext,
1570
+ 'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
1571
+ >,
1048
1572
  entry: _InternalRenderPipelineContext['validatedOrdered'][number],
1049
1573
  identityInstanceDraws: readonly GeometryInstanceDraw[],
1050
1574
  useProbeBlend: boolean,
@@ -1079,6 +1603,11 @@ function resolveGeometryInstanceBuffer(
1079
1603
  // A present, empty Instances array means zero draws. Do not fall back to
1080
1604
  // the shared identity buffer, which would silently render one instance.
1081
1605
  if (inst.instanceCount === 0) return [];
1606
+ const entityCacheKey = instanceCollectionCacheKey(entry.source.worldId, inst);
1607
+ const revision = inst.revision ?? inst.archVersion;
1608
+ const sameRevision = (cached: InstanceBufferCacheEntry): boolean =>
1609
+ cached.uploadedRevision === revision;
1610
+ const fullRange = [{ start: 0, end: inst.instanceCount }] as const;
1082
1611
  let instanceBuffer: Buffer = pipelineState.identityInstanceBuffer;
1083
1612
  let instanceCount = 1;
1084
1613
  {
@@ -1090,41 +1619,126 @@ function resolveGeometryInstanceBuffer(
1090
1619
  let instanceBufferUsage = GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST;
1091
1620
 
1092
1621
  if (uniformFallback && inst.instanceCount > MAX_UNIFORM_INSTANCES) {
1622
+ const ownerKey = instanceCollectionCacheKey(entry.source.worldId, inst);
1623
+ const chunks = frameState.instanceBufferChunks;
1624
+ const activeKeys = new Set<string>();
1093
1625
  const draws: GeometryInstanceDraw[] = [];
1626
+ const uniformStrideBytes = 16 * Float32Array.BYTES_PER_ELEMENT;
1627
+ const uploadedRanges: { start: number; end: number }[] = [];
1628
+ let uploadedBytes = 0;
1094
1629
  for (let start = 0; start < inst.instanceCount; start += MAX_UNIFORM_INSTANCES) {
1095
1630
  const count = Math.min(MAX_UNIFORM_INSTANCES, inst.instanceCount - start);
1096
- const chunk = inst.transforms.subarray(start * 16, (start + count) * 16);
1097
- const bufRes = runtime.device.createBuffer({
1098
- size: INSTANCE_UBO_FULL_ARRAY_BYTES,
1099
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1100
- mappedAtCreation: false,
1101
- });
1102
- if (!bufRes.ok) {
1103
- runtime.errorRegistry.fire(bufRes.error);
1104
- return null;
1631
+ const chunkKey = `${ownerKey}:${start}`;
1632
+ activeKeys.add(chunkKey);
1633
+ const payloadBytes = count * uniformStrideBytes;
1634
+ const previous = chunks?.get(chunkKey);
1635
+ let active =
1636
+ previous !== undefined &&
1637
+ previous.uploadedArchVersion === inst.archVersion &&
1638
+ previous.uploadedByteLength === payloadBytes
1639
+ ? previous
1640
+ : undefined;
1641
+ let activeIsNew = false;
1642
+ if (active === undefined) {
1643
+ const created = runtime.device.createBuffer({
1644
+ size: INSTANCE_UBO_FULL_ARRAY_BYTES,
1645
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1646
+ mappedAtCreation: false,
1647
+ });
1648
+ if (!created.ok) {
1649
+ runtime.errorRegistry.fire(created.error);
1650
+ reportInstanceFailure(
1651
+ c,
1652
+ inst,
1653
+ created.error,
1654
+ inst.instanceCount * uniformStrideBytes,
1655
+ MAX_UNIFORM_INSTANCES * uniformStrideBytes,
1656
+ );
1657
+ return null;
1658
+ }
1659
+ if (previous !== undefined && !previous.buffer.isDestroyed) {
1660
+ const destroyed = previous.buffer.destroy();
1661
+ if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
1662
+ }
1663
+ active = {
1664
+ buffer: new GpuBuffer(runtime.device, created.value),
1665
+ uploadedArchVersion: inst.archVersion,
1666
+ uploadedByteLength: payloadBytes,
1667
+ };
1668
+ activeIsNew = true;
1669
+ chunks?.set(chunkKey, active);
1670
+ if (chunks === undefined) frameState.transientInstanceBuffers.push(active);
1105
1671
  }
1106
- const buffer = new GpuBuffer(runtime.device, bufRes.value);
1107
- frameState.transientInstanceBuffers.push({
1108
- buffer,
1109
- uploadedArchVersion: inst.archVersion,
1110
- uploadedByteLength: chunk.byteLength,
1111
- });
1112
- const writeRes = runtime.device.queue.writeBuffer(buffer.handle, 0, chunk);
1113
- if (!writeRes.ok) {
1114
- runtime.errorRegistry.fire(writeRes.error);
1115
- return null;
1672
+ const needsUpload = activeIsNew || !sameRevision(active);
1673
+ const rangesToUpload = [
1674
+ {
1675
+ start: 0,
1676
+ end: count,
1677
+ },
1678
+ ];
1679
+ if (needsUpload && rangesToUpload.length > 0) {
1680
+ const upload = uploadInstanceRanges(
1681
+ c,
1682
+ inst,
1683
+ active.buffer.handle,
1684
+ rangesToUpload,
1685
+ false,
1686
+ start,
1687
+ );
1688
+ for (const range of upload.ranges) {
1689
+ uploadedRanges.push({ start: range.start + start, end: range.end + start });
1690
+ }
1691
+ uploadedBytes += upload.bytes;
1692
+ if (!upload.ok) {
1693
+ reportInstanceFailure(
1694
+ c,
1695
+ inst,
1696
+ {
1697
+ code: 'queue-write-buffer-failed',
1698
+ expected:
1699
+ 'the renderer-owned instance uniform buffer accepts the complete instance payload',
1700
+ hint: 'retry after the active device is healthy or recover the renderer',
1701
+ },
1702
+ inst.instanceCount * uniformStrideBytes,
1703
+ MAX_UNIFORM_INSTANCES * uniformStrideBytes,
1704
+ );
1705
+ return null;
1706
+ }
1707
+ }
1708
+ if (needsUpload) {
1709
+ const published = { ...active, uploadedRevision: revision };
1710
+ active = published;
1711
+ chunks?.set(chunkKey, published);
1116
1712
  }
1117
1713
  draws.push({
1118
- instanceBuffer: buffer.handle,
1714
+ instanceBuffer: active.buffer.handle,
1119
1715
  instanceBindGroup: resolveGeometryInstancesBindGroup(
1120
1716
  c,
1121
- buffer.handle,
1717
+ active.buffer.handle,
1122
1718
  probeBinding?.buffer,
1123
1719
  ),
1124
1720
  probeOffset: probeBinding?.offset ?? -1,
1125
1721
  instanceCount: count,
1126
1722
  });
1127
1723
  }
1724
+ if (chunks !== undefined) {
1725
+ const prefix = `${ownerKey}:`;
1726
+ for (const [key, stale] of chunks.entries()) {
1727
+ if (!key.startsWith(prefix) || activeKeys.has(key)) continue;
1728
+ if (!stale.buffer.isDestroyed) {
1729
+ const destroyed = stale.buffer.destroy();
1730
+ if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
1731
+ }
1732
+ chunks.delete(key);
1733
+ }
1734
+ }
1735
+ reportInstanceResidency(c, inst, {
1736
+ lane: 'chunked-uniform',
1737
+ requestedBytes: inst.instanceCount * uniformStrideBytes,
1738
+ supportedBytes: MAX_UNIFORM_INSTANCES * uniformStrideBytes,
1739
+ uploadRanges: uploadedRanges,
1740
+ uploadedBytes,
1741
+ });
1128
1742
  return draws;
1129
1743
  }
1130
1744
  if (uniformFallback) {
@@ -1134,31 +1748,30 @@ function resolveGeometryInstanceBuffer(
1134
1748
  {
1135
1749
  // Cap-gate (LimitExceededDetail single emit point — feat-20260514
1136
1750
  // M3 / w15 anchor): `requestedBytes <= maxStorageBufferBindingSize`.
1137
- const instancePayload = uniformFallback
1138
- ? inst.transforms
1139
- : packInstanceStorageBuffer(inst.transforms);
1140
- const requestedBytes = instancePayload.byteLength;
1751
+ // Derive the size from the authoring count; storage packing is deferred
1752
+ // until uploadInstanceRanges confirms a new or dirty resident.
1753
+ const requestedBytes =
1754
+ inst.instanceCount *
1755
+ (uniformFallback ? 16 : INSTANCE_STORAGE_STRIDE_FLOATS) *
1756
+ Float32Array.BYTES_PER_ELEMENT;
1141
1757
  const cap = runtime.device.limits.maxStorageBufferBindingSize;
1142
1758
  if (!uniformFallback && typeof cap === 'number' && cap > 0 && requestedBytes > cap) {
1143
- runtime.errorRegistry.fire(
1144
- new RhiError({
1145
- code: 'limit-exceeded',
1146
- expected: `requestedBytes (${requestedBytes}) <= maxStorageBufferBindingSize (${cap})`,
1147
- hint: 'reduce instance count to fit within device.limits.maxStorageBufferBindingSize, or split transforms across multiple Instances entries',
1148
- detail: {
1149
- maxStorageBufferBindingSize: cap,
1150
- requestedBytes,
1151
- },
1152
- }),
1759
+ const chunked = resolveStorageInstanceChunks(
1760
+ c,
1761
+ entry,
1762
+ inst,
1763
+ cap,
1764
+ probeBinding?.buffer,
1765
+ probeBinding?.offset ?? -1,
1153
1766
  );
1154
- return null;
1767
+ return chunked;
1155
1768
  } else {
1156
1769
  // Look up the cached GPU buffer or create a fresh one when the
1157
1770
  // archetype version bumped or the byte length changed.
1158
- const cached = frameState.instanceBuffers.get(
1159
- worldEntityKey(entry.source.worldId, inst.cacheKey),
1160
- );
1771
+ // entityCacheKey is the worldEntityKey + collectionId namespace.
1772
+ const cached = frameState.instanceBuffers.get(entityCacheKey);
1161
1773
  let active: InstanceBufferCacheEntry | null = null;
1774
+ let activeIsNew = false;
1162
1775
  if (
1163
1776
  cached !== undefined &&
1164
1777
  cached.uploadedArchVersion === inst.archVersion &&
@@ -1173,6 +1786,8 @@ function resolveGeometryInstanceBuffer(
1173
1786
  });
1174
1787
  if (!bufRes.ok) {
1175
1788
  runtime.errorRegistry.fire(bufRes.error);
1789
+ reportInstanceFailure(c, inst, bufRes.error, requestedBytes, cap);
1790
+ return null;
1176
1791
  } else {
1177
1792
  // feat-20260619 M4 / F12: destroy the old cached buffer
1178
1793
  // before replacing it with the new one (D-6).
@@ -1186,24 +1801,67 @@ function resolveGeometryInstanceBuffer(
1186
1801
  uploadedArchVersion: inst.archVersion,
1187
1802
  uploadedByteLength: requestedBytes,
1188
1803
  };
1189
- frameState.instanceBuffers.set(
1190
- worldEntityKey(entry.source.worldId, inst.cacheKey),
1191
- active,
1192
- );
1804
+ activeIsNew = true;
1805
+ if (active !== null) {
1806
+ // Keep the collection identity in the worldEntityKey namespace.
1807
+ frameState.instanceBuffers.set(entityCacheKey, active);
1808
+ }
1193
1809
  }
1194
1810
  }
1195
1811
  if (active !== null) {
1196
- const writeRes = runtime.device.queue.writeBuffer(
1197
- active.buffer.handle,
1198
- 0,
1199
- instancePayload,
1200
- );
1201
- if (!writeRes.ok) {
1202
- runtime.errorRegistry.fire(writeRes.error);
1203
- } else {
1204
- instanceBuffer = active.buffer.handle;
1205
- instanceCount = Math.max(1, inst.instanceCount);
1812
+ const needsUpload = activeIsNew || !sameRevision(active);
1813
+ const ranges = [fullRange[0]];
1814
+ let uploadRanges: readonly { readonly start: number; readonly end: number }[] = [];
1815
+ let uploadedBytes = 0;
1816
+ if (needsUpload && ranges.length > 0) {
1817
+ const upload = uploadInstanceRanges(
1818
+ c,
1819
+ inst,
1820
+ active.buffer.handle,
1821
+ ranges,
1822
+ !uniformFallback,
1823
+ );
1824
+ uploadRanges = upload.ranges;
1825
+ uploadedBytes = upload.bytes;
1826
+ if (!upload.ok) {
1827
+ reportInstanceFailure(
1828
+ c,
1829
+ inst,
1830
+ {
1831
+ code: 'queue-write-buffer-failed',
1832
+ expected:
1833
+ 'the renderer-owned instance buffer accepts the complete instance payloads',
1834
+ hint: 'retry after the active device is healthy or recover the renderer',
1835
+ },
1836
+ requestedBytes,
1837
+ uniformFallback
1838
+ ? MAX_UNIFORM_INSTANCES * Float32Array.BYTES_PER_ELEMENT * 16
1839
+ : typeof cap === 'number'
1840
+ ? cap
1841
+ : undefined,
1842
+ );
1843
+ return null;
1844
+ }
1206
1845
  }
1846
+ if (needsUpload) {
1847
+ const published = { ...active, uploadedRevision: revision };
1848
+ frameState.instanceBuffers.set(entityCacheKey, published);
1849
+ active = published;
1850
+ }
1851
+ instanceBuffer = active.buffer.handle;
1852
+ instanceCount = Math.max(1, inst.instanceCount);
1853
+ reportInstanceResidency(c, inst, {
1854
+ lane: uniformFallback ? 'direct-uniform' : 'direct-storage',
1855
+ requestedBytes,
1856
+ supportedBytes: uniformFallback
1857
+ ? MAX_UNIFORM_INSTANCES * Float32Array.BYTES_PER_ELEMENT * 16
1858
+ : typeof cap === 'number'
1859
+ ? cap
1860
+ : undefined,
1861
+ uploadRanges,
1862
+ uploadedBytes,
1863
+ });
1864
+ clearInstanceChunksForOwner(c, entityCacheKey);
1207
1865
  }
1208
1866
  }
1209
1867
  // The cache write above is keyed by worldEntityKey(worldId, cacheKey)
@@ -1221,7 +1879,10 @@ function resolveGeometryInstanceBuffer(
1221
1879
  }
1222
1880
 
1223
1881
  export function resolveProbeBlendBuffer(
1224
- c: _InternalRenderPipelineContext,
1882
+ c: Pick<
1883
+ _InternalRenderPipelineContext,
1884
+ 'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
1885
+ >,
1225
1886
  record: import('../scene/probe-blend-record').ProbeBlendRecord | undefined,
1226
1887
  entityKey?: number,
1227
1888
  ): { readonly buffer: Buffer; readonly offset: number } {
@@ -1269,7 +1930,10 @@ export function resolveProbeBlendBuffer(
1269
1930
  }
1270
1931
 
1271
1932
  export function resolveGeometryInstancesBindGroup(
1272
- c: _InternalRenderPipelineContext,
1933
+ c: Pick<
1934
+ _InternalRenderPipelineContext,
1935
+ 'runtime' | 'pipelineState' | 'frameState' | 'bindGroupCounts'
1936
+ >,
1273
1937
  instanceBuffer: Buffer,
1274
1938
  probeBuffer?: Buffer,
1275
1939
  ): BindGroup {