@forgeax/engine-render 0.1.32 → 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 (250) hide show
  1. package/README.md +379 -33
  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 +1 -1
  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-DRR7D3KX.mjs → chunk-3RTCJTH4.mjs} +12 -9
  21. package/dist/chunk-3RTCJTH4.mjs.map +1 -0
  22. package/dist/{chunk-S665NJNF.mjs → chunk-LEAXOMYJ.mjs} +3 -3
  23. package/dist/{chunk-S665NJNF.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
  24. package/dist/{chunk-6TFBXLRJ.mjs → chunk-NB2S7ZRX.mjs} +155 -11
  25. package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
  26. package/dist/{chunk-LULCA3OF.mjs → chunk-NNQL7CB3.mjs} +4 -4
  27. package/dist/{chunk-LULCA3OF.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
  28. package/dist/{chunk-4NNZQCL3.mjs → chunk-OO3DRYK7.mjs} +66 -200
  29. package/dist/chunk-OO3DRYK7.mjs.map +1 -0
  30. package/dist/{chunk-GWCEMDVR.mjs → chunk-PLAXOOX5.mjs} +232 -7
  31. package/dist/chunk-PLAXOOX5.mjs.map +1 -0
  32. package/dist/{chunk-JRIPCMYC.mjs → chunk-VOZWDM3L.mjs} +283 -193
  33. package/dist/chunk-VOZWDM3L.mjs.map +1 -0
  34. package/dist/{chunk-JCIRYQ4V.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/mesh-filter.d.ts +6 -5
  41. package/dist/components/mesh-filter.d.ts.map +1 -1
  42. package/dist/components/motion-blur.d.ts +1 -0
  43. package/dist/components/motion-blur.d.ts.map +1 -1
  44. package/dist/construct-renderer.mjs +2865 -526
  45. package/dist/construct-renderer.mjs.map +1 -1
  46. package/dist/device/gpu-residency.d.ts +6 -0
  47. package/dist/device/gpu-residency.d.ts.map +1 -1
  48. package/dist/environment/analytic-fog.d.ts +6 -0
  49. package/dist/environment/analytic-fog.d.ts.map +1 -0
  50. package/dist/errors/render.d.ts +6 -2
  51. package/dist/errors/render.d.ts.map +1 -1
  52. package/dist/extract/camera.d.ts.map +1 -1
  53. package/dist/extract/contracts.d.ts +7 -0
  54. package/dist/extract/contracts.d.ts.map +1 -1
  55. package/dist/extract/environment.d.ts +2 -0
  56. package/dist/extract/environment.d.ts.map +1 -1
  57. package/dist/features/error-reporting.d.ts +13 -0
  58. package/dist/features/error-reporting.d.ts.map +1 -0
  59. package/dist/features/host.d.ts +5 -0
  60. package/dist/features/host.d.ts.map +1 -1
  61. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  62. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  63. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  64. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  65. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  66. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  67. package/dist/features/plan.d.ts +7 -4
  68. package/dist/features/plan.d.ts.map +1 -1
  69. package/dist/features/prepared-gpu-work.d.ts +9 -5
  70. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  71. package/dist/features/render-graph-compute.d.ts.map +1 -1
  72. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  73. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  74. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  75. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  76. package/dist/gpu-driven/production-raster.d.ts +3 -1
  77. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  78. package/dist/gpu-driven/view-gpu.d.ts +9 -0
  79. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  80. package/dist/gpu-scene.d.ts +6 -1
  81. package/dist/gpu-scene.d.ts.map +1 -1
  82. package/dist/index.d.ts +5 -2
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.mjs +5 -5
  85. package/dist/inspection-types.d.ts +43 -2
  86. package/dist/inspection-types.d.ts.map +1 -1
  87. package/dist/instances.d.ts +8 -1
  88. package/dist/instances.d.ts.map +1 -1
  89. package/dist/internal.mjs +6 -6
  90. package/dist/pipeline/standard-post.d.ts +28 -19
  91. package/dist/pipeline/standard-post.d.ts.map +1 -1
  92. package/dist/pipeline-spec.d.ts +2 -2
  93. package/dist/record/frame-snapshot.d.ts +3 -0
  94. package/dist/record/frame-snapshot.d.ts.map +1 -1
  95. package/dist/record/frame-targets.d.ts.map +1 -1
  96. package/dist/record/frame.d.ts +5 -2
  97. package/dist/record/frame.d.ts.map +1 -1
  98. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  99. package/dist/record/mesh-ssbo.d.ts +6 -5
  100. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  101. package/dist/record/render-context.d.ts +12 -28
  102. package/dist/record/render-context.d.ts.map +1 -1
  103. package/dist/record/shadow-pass.d.ts.map +1 -1
  104. package/dist/record/skybox-post-pass.d.ts +5 -3
  105. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  106. package/dist/record/typed-frame-graph.d.ts +1 -1
  107. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  108. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  109. package/dist/render-contract.d.ts +24 -4
  110. package/dist/render-contract.d.ts.map +1 -1
  111. package/dist/render-pipeline.d.ts +3 -1
  112. package/dist/render-pipeline.d.ts.map +1 -1
  113. package/dist/render-system-extract-tail.d.ts.map +1 -1
  114. package/dist/render-system-extract.d.ts +8 -1
  115. package/dist/render-system-extract.d.ts.map +1 -1
  116. package/dist/render-system.d.ts +7 -1
  117. package/dist/render-system.d.ts.map +1 -1
  118. package/dist/scene/render-scene.d.ts +2 -0
  119. package/dist/scene/render-scene.d.ts.map +1 -1
  120. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  121. package/dist/temporal/frame.d.ts +7 -0
  122. package/dist/temporal/frame.d.ts.map +1 -1
  123. package/dist/temporal/index.mjs +5 -5
  124. package/dist/temporal/view.d.ts +6 -1
  125. package/dist/temporal/view.d.ts.map +1 -1
  126. package/dist/typed-render-graph-primitives.d.ts +10 -7
  127. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  128. package/dist/volume/component.d.ts +0 -1
  129. package/dist/volume/component.d.ts.map +1 -1
  130. package/dist/volume/passes.d.ts.map +1 -1
  131. package/dist/volume/temporal.d.ts +3 -1
  132. package/dist/volume/temporal.d.ts.map +1 -1
  133. package/package.json +21 -21
  134. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  135. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  136. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  137. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  138. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  139. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  140. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  141. package/src/__tests__/factory-contract.integration.test.ts +331 -68
  142. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  143. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  144. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  145. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
  146. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  147. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
  148. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  149. package/src/__tests__/gpu-scene.unit.test.ts +91 -0
  150. package/src/__tests__/instances-store.unit.test.ts +75 -0
  151. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  152. package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
  153. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
  154. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  155. package/src/__tests__/public-surface.test-d.ts +8 -0
  156. package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
  157. package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
  158. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  159. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
  160. package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
  161. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  162. package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
  163. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  164. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  165. package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
  166. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  167. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  168. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  169. package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
  170. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
  171. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
  172. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  173. package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
  174. package/src/assembly/analytic-fog-registration.ts +14 -0
  175. package/src/assembly/fxaa-registration.ts +17 -0
  176. package/src/assembly/lod-observation.ts +12 -0
  177. package/src/assembly/motion-blur-registration.ts +17 -0
  178. package/src/assembly/renderer-facade.ts +2 -0
  179. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  180. package/src/assembly/webgpu-ready-contract.ts +8 -3
  181. package/src/assembly/webgpu-ready.ts +181 -245
  182. package/src/assembly/webgpu-renderer.ts +16 -31
  183. package/src/bloom-admission.ts +21 -0
  184. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  185. package/src/components/atmosphere.ts +4 -0
  186. package/src/components/camera.ts +66 -10
  187. package/src/components/mesh-filter.ts +11 -9
  188. package/src/components/motion-blur.ts +1 -0
  189. package/src/device/gpu-residency.ts +56 -12
  190. package/src/environment/analytic-fog.ts +44 -0
  191. package/src/environment/background.ts +2 -2
  192. package/src/errors/render.ts +14 -1
  193. package/src/extract/camera.ts +20 -7
  194. package/src/extract/contracts.ts +7 -0
  195. package/src/extract/environment.ts +4 -0
  196. package/src/features/error-reporting.ts +78 -0
  197. package/src/features/host.ts +25 -12
  198. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  199. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  200. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  201. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  202. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  203. package/src/features/plan.ts +25 -8
  204. package/src/features/prepared-gpu-work.ts +86 -35
  205. package/src/features/render-graph-compute.ts +13 -5
  206. package/src/features/render-graph-contribution.ts +2 -0
  207. package/src/gpu-driven/material-artifacts.ts +2 -7
  208. package/src/gpu-driven/prepared-draw.ts +15 -10
  209. package/src/gpu-driven/production-raster.ts +83 -7
  210. package/src/gpu-driven/view-gpu.ts +17 -0
  211. package/src/gpu-scene.ts +188 -21
  212. package/src/index.ts +32 -0
  213. package/src/inspection-types.ts +60 -2
  214. package/src/instances.ts +80 -1
  215. package/src/pipeline/standard-post.ts +307 -78
  216. package/src/pipeline-spec.ts +5 -5
  217. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  218. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
  219. package/src/record/frame-snapshot.ts +5 -1
  220. package/src/record/frame-targets.ts +10 -6
  221. package/src/record/frame.ts +163 -22
  222. package/src/record/main-pass-geometry.ts +30 -1
  223. package/src/record/main-pass-sprite-draws.ts +6 -1
  224. package/src/record/mesh-ssbo.ts +43 -6
  225. package/src/record/render-context.ts +12 -28
  226. package/src/record/shadow-pass.ts +6 -1
  227. package/src/record/skybox-post-pass.ts +122 -196
  228. package/src/record/typed-frame-graph.ts +153 -9
  229. package/src/recovery/render-system-candidate.ts +22 -10
  230. package/src/render-contract.ts +23 -4
  231. package/src/render-pipeline.ts +6 -1
  232. package/src/render-system-extract-tail.ts +16 -0
  233. package/src/render-system-extract.ts +45 -13
  234. package/src/render-system.ts +255 -120
  235. package/src/scene/render-scene.ts +85 -4
  236. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  237. package/src/temporal/frame-coordinator.ts +8 -1
  238. package/src/temporal/frame.ts +20 -0
  239. package/src/temporal/view.ts +13 -2
  240. package/src/typed-render-graph-primitives.ts +121 -48
  241. package/src/volume/component.ts +0 -1
  242. package/src/volume/passes.ts +8 -4
  243. package/src/volume/resources.ts +1 -1
  244. package/src/volume/temporal.ts +11 -0
  245. package/dist/chunk-4NNZQCL3.mjs.map +0 -1
  246. package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
  247. package/dist/chunk-DRR7D3KX.mjs.map +0 -1
  248. package/dist/chunk-GWCEMDVR.mjs.map +0 -1
  249. package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
  250. package/dist/chunk-JRIPCMYC.mjs.map +0 -1
@@ -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();
@@ -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
+ });