@forgeax/engine-render 0.1.31 → 0.1.33

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 (293) hide show
  1. package/README.md +447 -34
  2. package/dist/assembly/analytic-fog-registration.d.ts +3 -0
  3. package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
  4. package/dist/assembly/fxaa-registration.d.ts +4 -0
  5. package/dist/assembly/fxaa-registration.d.ts.map +1 -0
  6. package/dist/assembly/lod-observation.d.ts +4 -0
  7. package/dist/assembly/lod-observation.d.ts.map +1 -0
  8. package/dist/assembly/motion-blur-registration.d.ts +3 -0
  9. package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
  10. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  11. package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
  12. package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
  13. package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
  14. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
  15. package/dist/assembly/webgpu-ready.d.ts +3 -2
  16. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  17. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  18. package/dist/bloom-admission.d.ts +13 -0
  19. package/dist/bloom-admission.d.ts.map +1 -0
  20. package/dist/{chunk-FUUO4UND.mjs → chunk-3RTCJTH4.mjs} +12 -9
  21. package/dist/chunk-3RTCJTH4.mjs.map +1 -0
  22. package/dist/{chunk-NO4WZCAH.mjs → chunk-LEAXOMYJ.mjs} +3 -3
  23. package/dist/{chunk-NO4WZCAH.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
  24. package/dist/{chunk-JRBDO2KB.mjs → chunk-NB2S7ZRX.mjs} +166 -10
  25. package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
  26. package/dist/{chunk-7MQ3XBSP.mjs → chunk-NNQL7CB3.mjs} +4 -4
  27. package/dist/{chunk-7MQ3XBSP.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
  28. package/dist/{chunk-C3DDSXYK.mjs → chunk-OO3DRYK7.mjs} +171 -230
  29. package/dist/chunk-OO3DRYK7.mjs.map +1 -0
  30. package/dist/{chunk-S5UE2T5P.mjs → chunk-PLAXOOX5.mjs} +608 -7
  31. package/dist/chunk-PLAXOOX5.mjs.map +1 -0
  32. package/dist/{chunk-PSEQ6QPH.mjs → chunk-VOZWDM3L.mjs} +295 -198
  33. package/dist/chunk-VOZWDM3L.mjs.map +1 -0
  34. package/dist/{chunk-MDQJPY3T.mjs → chunk-ZDQWGQW7.mjs} +10 -4
  35. package/dist/chunk-ZDQWGQW7.mjs.map +1 -0
  36. package/dist/components/atmosphere.d.ts +2 -0
  37. package/dist/components/atmosphere.d.ts.map +1 -1
  38. package/dist/components/camera.d.ts +20 -4
  39. package/dist/components/camera.d.ts.map +1 -1
  40. package/dist/components/depth-of-field.d.ts +28 -0
  41. package/dist/components/depth-of-field.d.ts.map +1 -0
  42. package/dist/components/directional-light.d.ts +2 -1
  43. package/dist/components/directional-light.d.ts.map +1 -1
  44. package/dist/components/index.d.ts +1 -0
  45. package/dist/components/index.d.ts.map +1 -1
  46. package/dist/components/mesh-filter.d.ts +6 -5
  47. package/dist/components/mesh-filter.d.ts.map +1 -1
  48. package/dist/components/motion-blur.d.ts +1 -0
  49. package/dist/components/motion-blur.d.ts.map +1 -1
  50. package/dist/construct-renderer.mjs +3746 -837
  51. package/dist/construct-renderer.mjs.map +1 -1
  52. package/dist/device/gpu-residency.d.ts +6 -0
  53. package/dist/device/gpu-residency.d.ts.map +1 -1
  54. package/dist/environment/analytic-fog.d.ts +6 -0
  55. package/dist/environment/analytic-fog.d.ts.map +1 -0
  56. package/dist/errors/render.d.ts +6 -2
  57. package/dist/errors/render.d.ts.map +1 -1
  58. package/dist/extract/camera.d.ts.map +1 -1
  59. package/dist/extract/contracts.d.ts +7 -0
  60. package/dist/extract/contracts.d.ts.map +1 -1
  61. package/dist/extract/environment.d.ts +2 -0
  62. package/dist/extract/environment.d.ts.map +1 -1
  63. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts +19 -0
  64. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts.map +1 -0
  65. package/dist/features/depth-of-field/depth-of-field-feature.d.ts +43 -0
  66. package/dist/features/depth-of-field/depth-of-field-feature.d.ts.map +1 -0
  67. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts +34 -0
  68. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts.map +1 -0
  69. package/dist/features/depth-of-field/depth-of-field-params.d.ts +83 -0
  70. package/dist/features/depth-of-field/depth-of-field-params.d.ts.map +1 -0
  71. package/dist/features/error-reporting.d.ts +13 -0
  72. package/dist/features/error-reporting.d.ts.map +1 -0
  73. package/dist/features/host.d.ts +5 -0
  74. package/dist/features/host.d.ts.map +1 -1
  75. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  76. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  77. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  78. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  79. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  80. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  81. package/dist/features/plan.d.ts +7 -4
  82. package/dist/features/plan.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +9 -5
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/render-graph-compute.d.ts.map +1 -1
  86. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  87. package/dist/features/types.d.ts +19 -1
  88. package/dist/features/types.d.ts.map +1 -1
  89. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  90. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  91. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  92. package/dist/gpu-driven/production-raster.d.ts +3 -1
  93. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  94. package/dist/gpu-driven/view-gpu.d.ts +9 -0
  95. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +6 -1
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/index.d.ts +8 -3
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.mjs +6 -5
  101. package/dist/index.mjs.map +1 -1
  102. package/dist/inspection-types.d.ts +78 -2
  103. package/dist/inspection-types.d.ts.map +1 -1
  104. package/dist/instances.d.ts +15 -1
  105. package/dist/instances.d.ts.map +1 -1
  106. package/dist/internal.mjs +6 -6
  107. package/dist/mesh-material-bindings.d.ts +3 -0
  108. package/dist/mesh-material-bindings.d.ts.map +1 -1
  109. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  110. package/dist/pipeline/standard-output/types.d.ts +4 -2
  111. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  112. package/dist/pipeline/standard-post.d.ts +28 -19
  113. package/dist/pipeline/standard-post.d.ts.map +1 -1
  114. package/dist/pipeline-spec.d.ts +2 -2
  115. package/dist/plugin.d.ts.map +1 -1
  116. package/dist/record/frame-snapshot.d.ts +24 -1
  117. package/dist/record/frame-snapshot.d.ts.map +1 -1
  118. package/dist/record/frame-targets.d.ts.map +1 -1
  119. package/dist/record/frame.d.ts +5 -2
  120. package/dist/record/frame.d.ts.map +1 -1
  121. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +6 -5
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/render-context.d.ts +12 -28
  126. package/dist/record/render-context.d.ts.map +1 -1
  127. package/dist/record/shadow-pass.d.ts.map +1 -1
  128. package/dist/record/skybox-post-pass.d.ts +5 -3
  129. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  130. package/dist/record/typed-frame-graph.d.ts +8 -1
  131. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  133. package/dist/render-contract.d.ts +32 -5
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-graph-primitives.d.ts +2 -0
  136. package/dist/render-graph-primitives.d.ts.map +1 -1
  137. package/dist/render-pipeline.d.ts +15 -1
  138. package/dist/render-pipeline.d.ts.map +1 -1
  139. package/dist/render-system-extract-tail.d.ts.map +1 -1
  140. package/dist/render-system-extract.d.ts +26 -5
  141. package/dist/render-system-extract.d.ts.map +1 -1
  142. package/dist/render-system.d.ts +9 -2
  143. package/dist/render-system.d.ts.map +1 -1
  144. package/dist/scene/render-scene.d.ts +2 -0
  145. package/dist/scene/render-scene.d.ts.map +1 -1
  146. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  147. package/dist/temporal/frame.d.ts +7 -0
  148. package/dist/temporal/frame.d.ts.map +1 -1
  149. package/dist/temporal/index.mjs +5 -5
  150. package/dist/temporal/view.d.ts +6 -1
  151. package/dist/temporal/view.d.ts.map +1 -1
  152. package/dist/typed-render-graph-primitives.d.ts +10 -7
  153. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  154. package/dist/volume/component.d.ts +0 -1
  155. package/dist/volume/component.d.ts.map +1 -1
  156. package/dist/volume/passes.d.ts.map +1 -1
  157. package/dist/volume/temporal.d.ts +3 -1
  158. package/dist/volume/temporal.d.ts.map +1 -1
  159. package/package.json +21 -21
  160. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  161. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  162. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  163. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  164. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  165. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  166. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  167. package/src/__tests__/factory-contract.integration.test.ts +331 -68
  168. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  169. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  170. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +12 -0
  171. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  172. package/src/__tests__/gpu-driven-production.integration.test.ts +27 -1
  173. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
  174. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  175. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
  176. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  177. package/src/__tests__/gpu-scene.unit.test.ts +91 -0
  178. package/src/__tests__/instances-store.unit.test.ts +90 -0
  179. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  180. package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
  181. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +49 -0
  182. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  183. package/src/__tests__/public-surface.test-d.ts +8 -0
  184. package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
  185. package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
  186. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  187. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
  188. package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
  189. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  190. package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
  191. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  192. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  193. package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
  194. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  195. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  196. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  197. package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
  198. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
  199. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
  200. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  201. package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
  202. package/src/assembly/analytic-fog-registration.ts +14 -0
  203. package/src/assembly/fxaa-registration.ts +17 -0
  204. package/src/assembly/lod-observation.ts +12 -0
  205. package/src/assembly/motion-blur-registration.ts +17 -0
  206. package/src/assembly/renderer-facade.ts +2 -0
  207. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  208. package/src/assembly/webgpu-ready-contract.ts +8 -3
  209. package/src/assembly/webgpu-ready.ts +186 -250
  210. package/src/assembly/webgpu-renderer.ts +30 -45
  211. package/src/bloom-admission.ts +21 -0
  212. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  213. package/src/components/atmosphere.ts +4 -0
  214. package/src/components/camera.ts +66 -10
  215. package/src/components/depth-of-field.ts +45 -0
  216. package/src/components/directional-light.ts +2 -1
  217. package/src/components/index.ts +1 -0
  218. package/src/components/mesh-filter.ts +11 -9
  219. package/src/components/motion-blur.ts +1 -0
  220. package/src/device/gpu-residency.ts +56 -12
  221. package/src/environment/analytic-fog.ts +44 -0
  222. package/src/environment/background.ts +2 -2
  223. package/src/errors/render.ts +14 -1
  224. package/src/extract/camera.ts +42 -9
  225. package/src/extract/contracts.ts +7 -0
  226. package/src/extract/environment.ts +4 -0
  227. package/src/features/depth-of-field/__tests__/depth-of-field-params.unit.test.ts +943 -0
  228. package/src/features/depth-of-field/depth-of-field-assembly.ts +100 -0
  229. package/src/features/depth-of-field/depth-of-field-feature.ts +295 -0
  230. package/src/features/depth-of-field/depth-of-field-inspection.ts +237 -0
  231. package/src/features/depth-of-field/depth-of-field-params.ts +556 -0
  232. package/src/features/error-reporting.ts +78 -0
  233. package/src/features/host.ts +26 -13
  234. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  235. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  236. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  237. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  238. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  239. package/src/features/plan.ts +25 -8
  240. package/src/features/prepared-gpu-work.ts +86 -35
  241. package/src/features/render-graph-compute.ts +13 -5
  242. package/src/features/render-graph-contribution.ts +2 -0
  243. package/src/features/types.ts +18 -1
  244. package/src/gpu-driven/material-artifacts.ts +2 -7
  245. package/src/gpu-driven/prepared-draw.ts +15 -10
  246. package/src/gpu-driven/production-raster.ts +83 -7
  247. package/src/gpu-driven/view-gpu.ts +17 -0
  248. package/src/gpu-scene.ts +188 -21
  249. package/src/index.ts +64 -0
  250. package/src/inspection-types.ts +98 -2
  251. package/src/instances.ts +93 -0
  252. package/src/mesh-material-bindings.ts +4 -0
  253. package/src/pipeline/standard-output/graph.ts +3 -0
  254. package/src/pipeline/standard-output/types.ts +4 -1
  255. package/src/pipeline/standard-post.ts +324 -76
  256. package/src/pipeline-spec.ts +5 -5
  257. package/src/plugin.ts +2 -0
  258. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  259. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
  260. package/src/record/frame-snapshot.ts +31 -2
  261. package/src/record/frame-targets.ts +10 -6
  262. package/src/record/frame.ts +202 -28
  263. package/src/record/main-pass-geometry.ts +30 -1
  264. package/src/record/main-pass-sprite-draws.ts +6 -1
  265. package/src/record/main-pass.ts +1 -5
  266. package/src/record/mesh-ssbo.ts +43 -6
  267. package/src/record/render-context.ts +12 -28
  268. package/src/record/shadow-pass.ts +6 -1
  269. package/src/record/skybox-post-pass.ts +122 -196
  270. package/src/record/typed-frame-graph.ts +259 -15
  271. package/src/recovery/render-system-candidate.ts +28 -10
  272. package/src/render-contract.ts +34 -4
  273. package/src/render-graph-primitives.ts +8 -1
  274. package/src/render-pipeline.ts +22 -1
  275. package/src/render-system-extract-tail.ts +103 -13
  276. package/src/render-system-extract.ts +148 -38
  277. package/src/render-system.ts +332 -119
  278. package/src/scene/render-scene.ts +85 -4
  279. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  280. package/src/temporal/frame-coordinator.ts +8 -1
  281. package/src/temporal/frame.ts +20 -0
  282. package/src/temporal/view.ts +13 -2
  283. package/src/typed-render-graph-primitives.ts +124 -48
  284. package/src/volume/component.ts +0 -1
  285. package/src/volume/passes.ts +8 -4
  286. package/src/volume/resources.ts +1 -1
  287. package/src/volume/temporal.ts +11 -0
  288. package/dist/chunk-C3DDSXYK.mjs.map +0 -1
  289. package/dist/chunk-FUUO4UND.mjs.map +0 -1
  290. package/dist/chunk-JRBDO2KB.mjs.map +0 -1
  291. package/dist/chunk-MDQJPY3T.mjs.map +0 -1
  292. package/dist/chunk-PSEQ6QPH.mjs.map +0 -1
  293. package/dist/chunk-S5UE2T5P.mjs.map +0 -1
@@ -6,6 +6,7 @@ const read = (path: string): string => readFileSync(new URL(path, import.meta.ur
6
6
  const count = (source: string, pattern: RegExp): number => source.match(pattern)?.length ?? 0;
7
7
 
8
8
  const productionRaster = read('../gpu-driven/production-raster.ts');
9
+ const mainPass = read('../record/main-pass.ts');
9
10
  const shadowPass = read('../record/shadow-pass.ts');
10
11
  const paletteAllocator = read('../systems/skin-palette-allocator.ts');
11
12
  const extractRoot = read('../render-system-extract.ts');
@@ -46,6 +47,17 @@ describe('GPU-driven baseline characterization', () => {
46
47
  expect(productionRaster).toContain('vertexLayout.arrayStride');
47
48
  });
48
49
 
50
+ it('keeps CPU-only video slots out of the global GPU receipt gate', () => {
51
+ expect(mainPass).toContain('videoTextureFields?.size');
52
+ expect(mainPass).toContain('if (gpuDrivenStandardPbrFrameResources === undefined)');
53
+ expect(mainPass).not.toContain(
54
+ 'gpuDrivenStandardPbrFrameResources.materialBindGroups.some((group) => group === undefined)',
55
+ );
56
+ expect(productionRaster).toMatch(
57
+ /material bind group for global slot \$\{globalMaterialSlot\}/,
58
+ );
59
+ });
60
+
49
61
  it('freezes CPU shadow caster enumeration as the pre-GPU baseline', () => {
50
62
  expect(shadowPass).toContain('validatedOrdered');
51
63
  expect(shadowPass).toMatch(/for \(let i = 0; i < validatedOrdered\.length; i\+\+\)/);
@@ -110,7 +110,8 @@ function camera(): CameraSnapshot {
110
110
  bloom: 'off',
111
111
  bloomThreshold: 1,
112
112
  bloomIntensity: 1,
113
- bloomBlurRadius: 4,
113
+ bloomSoftKnee: 0.5,
114
+ bloomScatter: 0.7,
114
115
  clearColor: [0, 0, 0, 0],
115
116
  };
116
117
  }
@@ -824,6 +824,7 @@ describe('GPU-driven production projection', () => {
824
824
  device.createBindGroup({ layout: materialBindGroup, entries: [] }).unwrap(),
825
825
  ],
826
826
  };
827
+ let activeFrameResources = frameResources;
827
828
  const color = graph
828
829
  .createTexture('gpu-driven-production-color', {
829
830
  format: 'rgba8unorm',
@@ -842,7 +843,8 @@ describe('GPU-driven production projection', () => {
842
843
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
843
844
  },
844
845
  ],
845
- encode: ({ pass, resources }) => gpu.encode(viewBindGroup, pass, resources, frameResources),
846
+ encode: ({ pass, resources }) =>
847
+ gpu.encode(viewBindGroup, pass, resources, activeFrameResources),
846
848
  })
847
849
  .unwrap();
848
850
  const compiled = graph.compile({ device, surfaceSize: { width: 1, height: 1 } }).unwrap();
@@ -872,6 +874,30 @@ describe('GPU-driven production projection', () => {
872
874
  production.inspect().batchCount,
873
875
  );
874
876
 
877
+ const missingMaterialFrameResources = {
878
+ ...frameResources,
879
+ materialBindGroups: [undefined] as unknown as typeof frameResources.materialBindGroups,
880
+ };
881
+ activeFrameResources = missingMaterialFrameResources;
882
+ const missingMaterialEncoder = device
883
+ .createCommandEncoder({ label: 'gpu-driven-production-missing-material' })
884
+ .unwrap();
885
+ const missingMaterialResult = compiled.execute({
886
+ viewBindGroup,
887
+ encoder: missingMaterialEncoder,
888
+ gpuDrivenStandardPbrFrameResources: missingMaterialFrameResources,
889
+ } as unknown as RenderPipelineFrame);
890
+ expect(missingMaterialResult.ok).toBe(false);
891
+ if (!missingMaterialResult.ok) {
892
+ expect(missingMaterialResult.error.code).toBe('pass-encode-failed');
893
+ const detail = missingMaterialResult.error.detail;
894
+ expect(detail).toBeDefined();
895
+ expect(detail !== undefined && 'cause' in detail).toBe(true);
896
+ if (detail === undefined || !('cause' in detail)) return;
897
+ expect(detail.cause).toBeInstanceOf(RhiError);
898
+ expect((detail.cause as RhiError).code).toBe('rhi-not-available');
899
+ }
900
+
875
901
  // The facet projection is the same admission set consumed by the GPU
876
902
  // candidate upload. A suppressed entity must disappear from the indirect
877
903
  // plan without making the remaining entity fall back to CPU validation.
@@ -68,7 +68,7 @@ function topology(): RenderPipelineTopology {
68
68
  storageFormat: 'rgba8unorm',
69
69
  viewFormat: 'rgba8unorm',
70
70
  },
71
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'off' },
71
+ camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'off', bloomIntensity: 1 },
72
72
  shadow: {
73
73
  directional: { mapSize: 32, cascadeCount: 2 },
74
74
  spotMapSize: 32,
@@ -24,7 +24,7 @@ function topology(): RenderPipelineTopology {
24
24
  storageFormat: 'rgba8unorm',
25
25
  viewFormat: 'rgba8unorm',
26
26
  },
27
- camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'off' },
27
+ camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'off', bloomIntensity: 1 },
28
28
  shadow: {
29
29
  directional: { mapSize: 32, cascadeCount: 2 },
30
30
  spotMapSize: 32,
@@ -247,6 +247,7 @@ export async function runGpuDrivenViewGpuEvidence(): Promise<GpuDrivenViewGpuEvi
247
247
  })
248
248
  .unwrap();
249
249
  const graph = new RenderGraphBuilder<{ readonly encoder: RhiCommandEncoder }>();
250
+ view.setTelemetrySubmit({ frameId: 1, deviceGeneration: 0 });
250
251
  view.addPasses(graph).unwrap();
251
252
  const compiled = graph.compile({ device, surfaceSize: { width: 1, height: 1 } }).unwrap();
252
253
  const encoder = device.createCommandEncoder({ label: 'gpu-driven-evidence' }).unwrap();
@@ -279,6 +280,9 @@ export async function runGpuDrivenViewGpuEvidence(): Promise<GpuDrivenViewGpuEvi
279
280
  if (lodSelection === undefined || lodSelection.batches.length !== lodSelection.batchCount) {
280
281
  throw new Error('GPU-driven LOD selection readback unavailable or incomplete');
281
282
  }
283
+ if (lodSelection.submit?.frameId !== 1 || lodSelection.submit.deviceGeneration !== 0) {
284
+ throw new Error('GPU-driven LOD telemetry lost the copy submit identity');
285
+ }
282
286
  await device.queue.onSubmittedWorkDone();
283
287
 
284
288
  // After unmap, a later cached-graph submission must restore telemetry
@@ -292,6 +296,7 @@ export async function runGpuDrivenViewGpuEvidence(): Promise<GpuDrivenViewGpuEvi
292
296
  frustum.fromViewProjection(frustum.create(), recoveryProjection),
293
297
  )
294
298
  .unwrap();
299
+ view.setTelemetrySubmit({ frameId: 2, deviceGeneration: 0 });
295
300
  const recoveryEncoder = device
296
301
  .createCommandEncoder({ label: 'gpu-driven-evidence-recovery' })
297
302
  .unwrap();
@@ -306,6 +311,12 @@ export async function runGpuDrivenViewGpuEvidence(): Promise<GpuDrivenViewGpuEvi
306
311
  ) {
307
312
  throw new Error('GPU-driven LOD telemetry did not recover after a skipped copy');
308
313
  }
314
+ if (
315
+ recoveredLodSelection.submit?.frameId !== 2 ||
316
+ recoveredLodSelection.submit.deviceGeneration !== 0
317
+ ) {
318
+ throw new Error('GPU-driven LOD telemetry reused a stale submit identity');
319
+ }
309
320
  const validationError = await rawDevice.popErrorScope();
310
321
  if (validationError !== null) {
311
322
  throw new Error(`GPU-driven telemetry overlap validation failed: ${validationError.message}`);
@@ -107,6 +107,13 @@ describe('GpuResidencyCache mesh stride', () => {
107
107
  };
108
108
  const world = new World();
109
109
  const handle = world.allocSharedRef('MeshAsset', root);
110
+ // The root can be pulled before the lower pack finishes loading. A later
111
+ // call with the complete chain must upgrade the same cache key.
112
+ const rootOnly = store.ensureResident(handle, root);
113
+ expect(rootOnly.ok).toBe(true);
114
+ if (!rootOnly.ok) return;
115
+ expect(rootOnly.value.lodRanges).toBeUndefined();
116
+
110
117
  const resident = store.ensureResident(handle, root, 0, [lower]);
111
118
  expect(resident.ok).toBe(true);
112
119
  if (!resident.ok) return;
@@ -120,6 +127,11 @@ describe('GpuResidencyCache mesh stride', () => {
120
127
  expect(packedIndices).toBeDefined();
121
128
  if (packedIndices === undefined) return;
122
129
  expect(new Uint16Array(packedIndices.buffer)).toEqual(new Uint16Array([0, 1, 2, 0, 2, 1]));
130
+
131
+ // A stable chain remains an O(1) hit and does not replace the entry again.
132
+ const stable = store.ensureResident(handle, root, 0, [lower]);
133
+ expect(stable.ok).toBe(true);
134
+ if (stable.ok) expect(stable.value).toBe(resident.value);
123
135
  });
124
136
 
125
137
  it('refuses a payload whose bytes do not match the projected stride', async () => {
@@ -126,6 +126,34 @@ function instanceSnapshot(entityKey: number, translationX: number): RenderableSn
126
126
  };
127
127
  }
128
128
 
129
+ function multiInstanceSnapshot(
130
+ entityKey: number,
131
+ translations: readonly number[],
132
+ generations: readonly number[],
133
+ ): RenderableSnapshot {
134
+ const base = snapshot(entityKey);
135
+ const transforms = new Float32Array(translations.length * 16);
136
+ for (let ordinal = 0; ordinal < translations.length; ordinal += 1) {
137
+ const offset = ordinal * 16;
138
+ transforms[offset] = 1;
139
+ transforms[offset + 5] = 1;
140
+ transforms[offset + 10] = 1;
141
+ transforms[offset + 12] = translations[ordinal] ?? 0;
142
+ transforms[offset + 15] = 1;
143
+ }
144
+ return {
145
+ ...base,
146
+ instances: {
147
+ transforms,
148
+ generations: new Uint32Array(generations),
149
+ instanceCount: translations.length,
150
+ cacheKey: entityKey,
151
+ archVersion: 1,
152
+ revision: 1,
153
+ },
154
+ };
155
+ }
156
+
129
157
  function persistentFrame(
130
158
  renderables: readonly RenderableSnapshot[],
131
159
  cameras: ExtractedFrame['cameras'] = [],
@@ -399,6 +427,69 @@ describe('GpuScene', () => {
399
427
  });
400
428
  });
401
429
 
430
+ it('maps GPU previous instance transforms by stable generation after reorder', () => {
431
+ const queue = new RecordingQueue();
432
+ const created = GpuScene.create(createDevice(queue), 4).unwrap();
433
+ if (created.status !== 'available') return;
434
+ const projection = new RenderScene();
435
+ projection.setTemporalTracking(true);
436
+ const initial = multiInstanceSnapshot(0, [2, 8], [101, 202]);
437
+ created.scene.sync(projection.apply([updateSnapshot(initial)])).unwrap();
438
+ projection.commitSubmission(projection.captureSubmission([{ worldId: 0, entityKey: 0 }]));
439
+
440
+ const reordered = multiInstanceSnapshot(0, [9, 3], [202, 101]);
441
+ const delta = projection.apply([updateInstances(reordered)]);
442
+ const slot = projection.slotsSnapshot()[0];
443
+ expect(slot).toBeDefined();
444
+ if (slot === undefined) return;
445
+ expect(projection.temporalSnapshotBySlot(slot.slot)?.motionValid).toBe(true);
446
+
447
+ queue.writes.length = 0;
448
+ created.scene
449
+ .sync(delta, (current) => projection.temporalSnapshotBySlot(current.slot))
450
+ .unwrap();
451
+ const transformWrite = queue.writes.find(
452
+ ({ bytes }) => bytes.byteLength === 2 * GPU_SCENE_LAYOUTS.transform.stride,
453
+ );
454
+ expect(transformWrite).toBeDefined();
455
+ if (transformWrite === undefined) return;
456
+ const previousTranslation =
457
+ gpuSceneFieldOffset(GPU_SCENE_LAYOUTS.transform, 'previousWorld') + 12 * 4;
458
+ const transforms = new DataView(transformWrite.bytes.buffer);
459
+ expect(transforms.getFloat32(previousTranslation, true)).toBe(8);
460
+ expect(
461
+ transforms.getFloat32(GPU_SCENE_LAYOUTS.transform.stride + previousTranslation, true),
462
+ ).toBe(2);
463
+
464
+ created.scene.commitTemporalFrame().unwrap();
465
+ projection.commitSubmission(projection.captureSubmission([{ worldId: 0, entityKey: 0 }]));
466
+ const retired = multiInstanceSnapshot(0, [11], [202]);
467
+ const retiredDelta = projection.apply([updateInstances(retired)]);
468
+ expect(projection.temporalSnapshotBySlot(slot.slot)?.motionValid).toBe(true);
469
+ queue.writes.length = 0;
470
+ created.scene
471
+ .sync(retiredDelta, (current) => projection.temporalSnapshotBySlot(current.slot))
472
+ .unwrap();
473
+ const compactedRows = queue.writes
474
+ .filter(({ bytes }) => bytes.byteLength % GPU_SCENE_LAYOUTS.transform.stride === 0)
475
+ .flatMap(({ bytes }) =>
476
+ Array.from({ length: bytes.byteLength / GPU_SCENE_LAYOUTS.transform.stride }, (_, row) => {
477
+ const view = new DataView(bytes.buffer);
478
+ return {
479
+ current: view.getFloat32(
480
+ row * GPU_SCENE_LAYOUTS.transform.stride + currentWorldOffset() + 12 * 4,
481
+ true,
482
+ ),
483
+ previous: view.getFloat32(
484
+ row * GPU_SCENE_LAYOUTS.transform.stride + previousTranslation,
485
+ true,
486
+ ),
487
+ };
488
+ }),
489
+ );
490
+ expect(compactedRows).toContainEqual({ current: 11, previous: 9 });
491
+ });
492
+
402
493
  it('advances GPU previous transforms after a successful temporal commit', () => {
403
494
  const queue = new RecordingQueue();
404
495
  const created = GpuScene.create(createDevice(queue), 4).unwrap();
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { InstanceCollectionStore, instanceUploadRangesForResident } from '../instances';
3
3
  import { instanceCollectionCacheKey } from '../record/frame-snapshot';
4
+ import { packInstanceStorageBuffer } from '../record/mesh-ssbo';
4
5
 
5
6
  function identityTransforms(count: number): Float32Array {
6
7
  const transforms = new Float32Array(count * 16);
@@ -14,6 +15,14 @@ function identityTransforms(count: number): Float32Array {
14
15
  return transforms;
15
16
  }
16
17
 
18
+ function translatedTransforms(...translations: readonly number[]): Float32Array {
19
+ const transforms = identityTransforms(translations.length);
20
+ for (const [index, translation] of translations.entries()) {
21
+ transforms[index * 16 + 12] = translation;
22
+ }
23
+ return transforms;
24
+ }
25
+
17
26
  describe('InstanceCollectionStore', () => {
18
27
  it('keeps large payloads outside ECS-sized storage and preserves one identity', () => {
19
28
  const store = new InstanceCollectionStore();
@@ -40,6 +49,72 @@ describe('InstanceCollectionStore', () => {
40
49
  expect(after.transforms[3 * 16 + 12]).toBe(4);
41
50
  });
42
51
 
52
+ it('keeps identity generations stable across pose updates and seeds topology changes', () => {
53
+ const store = new InstanceCollectionStore();
54
+ const created = store.create({ transforms: translatedTransforms(1, 2) }).unwrap();
55
+ const before = store.snapshot(created.collectionId).unwrap();
56
+
57
+ store
58
+ .update(created.collectionId, {
59
+ start: 1,
60
+ transforms: translatedTransforms(9),
61
+ })
62
+ .unwrap();
63
+ const moved = store.snapshot(created.collectionId).unwrap();
64
+ expect([...moved.generations]).toEqual([...before.generations]);
65
+ const movedPayload = packInstanceStorageBuffer(
66
+ moved.transforms,
67
+ before.transforms,
68
+ moved.generations,
69
+ before.generations,
70
+ );
71
+ expect(movedPayload[16 + 12]).toBe(1);
72
+ expect(movedPayload[32 + 12]).toBe(9);
73
+ expect(movedPayload[32 + 16 + 12]).toBe(2);
74
+
75
+ store.replace(created.collectionId, translatedTransforms(20)).unwrap();
76
+ const compacted = store.snapshot(created.collectionId).unwrap();
77
+ expect(compacted.generations[0]).toBeGreaterThan(moved.generations[0] ?? 0);
78
+ const compactedPayload = packInstanceStorageBuffer(
79
+ compacted.transforms,
80
+ moved.transforms,
81
+ compacted.generations,
82
+ moved.generations,
83
+ );
84
+ // The surviving ordinal was replaced/compacted, so its previous slot is
85
+ // seeded from the current pose instead of inheriting instance 0 or 1.
86
+ expect(compactedPayload[16 + 12]).toBe(20);
87
+ });
88
+
89
+ it('maps real previous transforms through an explicit compaction reorder', () => {
90
+ const store = new InstanceCollectionStore();
91
+ const created = store.create({ transforms: translatedTransforms(1, 2) }).unwrap();
92
+ const before = store.snapshot(created.collectionId).unwrap();
93
+ const reordered = store
94
+ .replace(
95
+ created.collectionId,
96
+ translatedTransforms(20, 10),
97
+ new Uint32Array([before.generations[1] ?? 0, before.generations[0] ?? 0]),
98
+ )
99
+ .unwrap();
100
+ const after = store.snapshot(created.collectionId).unwrap();
101
+
102
+ expect(after.generations).toEqual(
103
+ new Uint32Array([before.generations[1] ?? 0, before.generations[0] ?? 0]),
104
+ );
105
+ const payload = packInstanceStorageBuffer(
106
+ after.transforms,
107
+ before.transforms,
108
+ after.generations,
109
+ before.generations,
110
+ );
111
+ expect(payload[12]).toBe(20);
112
+ expect(payload[16 + 12]).toBe(2);
113
+ expect(payload[32 + 12]).toBe(10);
114
+ expect(payload[32 + 16 + 12]).toBe(1);
115
+ expect(reordered.count).toBe(2);
116
+ });
117
+
43
118
  it('keeps the full union when partial updates overlap out of order', () => {
44
119
  const store = new InstanceCollectionStore();
45
120
  const created = store.create({ transforms: identityTransforms(16) }).unwrap();
@@ -51,6 +126,21 @@ describe('InstanceCollectionStore', () => {
51
126
  ]);
52
127
  });
53
128
 
129
+ it('keeps renderer dirty ranges intact when inspection peeks between updates', () => {
130
+ const store = new InstanceCollectionStore();
131
+ const created = store.create({ transforms: identityTransforms(2) }).unwrap();
132
+ store.snapshot(created.collectionId).unwrap();
133
+
134
+ store.update(created.collectionId, { start: 0, transforms: identityTransforms(1) }).unwrap();
135
+ const inspected = store.peek(created.collectionId).unwrap();
136
+ expect(inspected.dirtyRanges).toEqual([{ start: 0, end: 1 }]);
137
+ store.update(created.collectionId, { start: 1, transforms: identityTransforms(1) }).unwrap();
138
+
139
+ expect(store.snapshot(created.collectionId).unwrap().dirtyRanges).toEqual([
140
+ { start: 0, end: 2 },
141
+ ]);
142
+ });
143
+
54
144
  it('keeps collection identity in the renderer cache key', () => {
55
145
  const first = instanceCollectionCacheKey(0, { cacheKey: 9, collectionId: 1 });
56
146
  const second = instanceCollectionCacheKey(0, { cacheKey: 9, collectionId: 2 });
@@ -59,3 +59,34 @@ it('fences ordinary mesh submissions independently of candidates, including reve
59
59
  expect(replacement.vertexBuffer.isDestroyed).toBe(true);
60
60
  expect(scope.resourceDelta()).toBe(0);
61
61
  });
62
+
63
+ it('separates recycled shared mesh generations while old GPU work is still retained', async () => {
64
+ const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
65
+ const store = new GpuResidencyCache();
66
+ store.configureGpuDevice(
67
+ device,
68
+ undefined,
69
+ () => {
70
+ throw new Error('no cubemap');
71
+ },
72
+ device.caps,
73
+ );
74
+ const world = new World();
75
+ const first = createBoxGeometry(1, 1, 1).unwrap();
76
+ const oldHandle = world.allocSharedRef('MeshAsset', first);
77
+ const oldMesh = store.ensureResident(oldHandle, first, world).unwrap();
78
+ const lease = store.retainMeshResidency(oldHandle, world);
79
+ if (lease === undefined) throw new Error('mesh residency missing');
80
+ world.sharedRefs.release(oldHandle).unwrap();
81
+ const next = createBoxGeometry(8, 8, 8).unwrap();
82
+ const newHandle = world.allocSharedRef('MeshAsset', next);
83
+ expect(Number(newHandle) & 0xffffff).toBe(Number(oldHandle) & 0xffffff);
84
+ expect(newHandle).not.toBe(oldHandle);
85
+ const newMesh = store.ensureResident(newHandle, next, world).unwrap();
86
+ expect(newMesh).not.toBe(oldMesh);
87
+ store.invalidateMesh(oldHandle, world);
88
+ lease.release(true);
89
+ expect(store.getMeshGpuHandles(newHandle, world)).toBe(newMesh);
90
+ expect(newMesh.vertexBuffer.isDestroyed).toBe(false);
91
+ store.destroyAll();
92
+ });