@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
@@ -92,6 +92,10 @@ export type BloomGraphStatus = 'empty' | 'valid' | 'invalid';
92
92
  export interface BloomInspection {
93
93
  readonly graphStatus: BloomGraphStatus;
94
94
  readonly enabled: boolean;
95
+ readonly levelCount: number;
96
+ readonly levelDimensions: readonly { readonly width: number; readonly height: number }[];
97
+ readonly downsamplePassCount: number;
98
+ readonly upsamplePassCount: number;
95
99
  readonly targetCount: number;
96
100
  readonly targetBytes: number;
97
101
  readonly resourceCount: number;
@@ -99,10 +103,30 @@ export interface BloomInspection {
99
103
  readonly encodeCount: number;
100
104
  readonly bindGroupCount: number;
101
105
  readonly uploadCount: number;
106
+ /** Descriptor-derived bytes for the active Bloom target child in the compiled graph. */
102
107
  readonly residentChildBytes: number;
103
108
  readonly generation: number;
104
109
  readonly state: 'off' | 'active' | 'retiring';
105
110
  }
111
+
112
+ export const emptyBloomInspection = (): BloomInspection => ({
113
+ graphStatus: 'empty',
114
+ enabled: false,
115
+ levelCount: 0,
116
+ levelDimensions: [],
117
+ downsamplePassCount: 0,
118
+ upsamplePassCount: 0,
119
+ targetCount: 0,
120
+ targetBytes: 0,
121
+ resourceCount: 0,
122
+ passCount: 0,
123
+ encodeCount: 0,
124
+ bindGroupCount: 0,
125
+ uploadCount: 0,
126
+ residentChildBytes: 0,
127
+ generation: 0,
128
+ state: 'off',
129
+ });
106
130
  export type { TransmissionInspection } from './transmission/inspection';
107
131
 
108
132
  export interface BatchTopologyInspection {
@@ -555,7 +579,29 @@ export interface TemporalTargetInspection {
555
579
  }
556
580
 
557
581
  /** Detached, bounded Motion Blur facts; no graph, device, target, or history handle. */
558
- export type MotionBlurInspectionStatus = 'off' | 'active' | 'reset' | 'invalid';
582
+ export type MotionBlurInspectionStatus = 'off' | 'active' | 'reset' | 'limited' | 'invalid';
583
+ export type MotionBlurInspectionLane = 'compute' | 'raster-limited';
584
+
585
+ /**
586
+ * Submission receipt for the admitted compute lane. This is derived from the
587
+ * compiled graph and the resolved feature dispatches after a successful
588
+ * queue submit; capability intent alone never produces this receipt.
589
+ */
590
+ export interface MotionBlurExecutionPass {
591
+ readonly name: 'motion-blur-tile-summary' | 'motion-blur';
592
+ readonly kind: 'compute';
593
+ readonly entryPoint: 'tile_summary' | 'reconstruct';
594
+ readonly workgroups: readonly [number, number, number];
595
+ }
596
+
597
+ export interface MotionBlurExecutionReceipt {
598
+ readonly frameId: number;
599
+ readonly deviceGeneration: number;
600
+ readonly graphGeneration: number;
601
+ readonly program: 'motion-blur-compute';
602
+ readonly outputFormat: 'rgba16float';
603
+ readonly passes: readonly [MotionBlurExecutionPass, MotionBlurExecutionPass];
604
+ }
559
605
 
560
606
  /** Bounded renderer-owned Depth of Field facts; no graph or GPU handles. */
561
607
  export type DepthOfFieldInspectionStatus = 'off' | 'active' | 'reset' | 'invalid' | 'unsupported';
@@ -597,13 +643,25 @@ export interface DepthOfFieldInspection {
597
643
  export interface MotionBlurInspection {
598
644
  readonly enabled: boolean;
599
645
  readonly status: MotionBlurInspectionStatus;
646
+ /** Capability-selected implementation lane; raster is explicitly limited. */
647
+ readonly lane: MotionBlurInspectionLane;
600
648
  readonly shutterAngle: number;
601
649
  readonly maxRadiusPixels: number;
602
650
  readonly sampleCount: number;
651
+ /** Target presentation rate used by the accepted exposure calculation. */
652
+ readonly targetFps: number;
653
+ /** Bounded tier selected from the authored sample budget. */
654
+ readonly effectiveSampleCount: 0 | 4 | 8 | 16;
603
655
  readonly temporalDemand: 'none' | 'scene-data-temporal-v1';
604
656
  readonly passName: 'motion-blur';
605
657
  readonly historyWrites: 0;
606
- readonly lastFailure?: 'invalid-params' | 'scene-data-unavailable';
658
+ /** Zero when demand is disabled; compute uses one summary plus one fused pass. */
659
+ readonly passCount: 0 | 1 | 2;
660
+ readonly tapBudget: 0 | 4 | 8 | 16;
661
+ /** Present only when the current accepted frame executed both compute passes. */
662
+ readonly execution?: MotionBlurExecutionReceipt;
663
+ readonly resetReason?: import('./temporal/view').TemporalResetReason;
664
+ readonly lastFailure?: 'invalid-params' | 'scene-data-unavailable' | 'submit-failure';
607
665
  }
608
666
 
609
667
  export type RenderSceneResyncReason =
package/src/instances.ts CHANGED
@@ -41,6 +41,8 @@ export function instanceUploadRangesForResident(
41
41
 
42
42
  export interface InstanceCollectionSnapshot extends InstanceCollectionInfo {
43
43
  readonly transforms: Float32Array;
44
+ /** Unique identity generations that travel with an instance through reorder. */
45
+ readonly generations: Uint32Array;
44
46
  readonly dirtyRanges: readonly InstanceDirtyRange[];
45
47
  }
46
48
 
@@ -171,6 +173,7 @@ export type InstanceCollectionResult<T> = Result<T, InstanceCollectionError>;
171
173
  interface CollectionRecord {
172
174
  readonly id: InstanceCollectionId;
173
175
  transforms: Float32Array;
176
+ generations: Uint32Array;
174
177
  count: number;
175
178
  capacity: number;
176
179
  revision: number;
@@ -307,6 +310,7 @@ function missing(
307
310
  */
308
311
  export class InstanceCollectionStore {
309
312
  private nextId = 1;
313
+ private nextInstanceGeneration = 1;
310
314
  private epochValue = 0;
311
315
  private disposed = false;
312
316
  private readonly records = new Map<InstanceCollectionId, CollectionRecord>();
@@ -330,9 +334,11 @@ export class InstanceCollectionStore {
330
334
  if (invalidPayload !== undefined) return err(invalidPayload);
331
335
  const id = this.nextId++ as InstanceCollectionId;
332
336
  const count = input.transforms.length / MATRIX_STRIDE;
337
+ const generations = this.allocateGenerations(count);
333
338
  const record: CollectionRecord = {
334
339
  id,
335
340
  transforms: new Float32Array(input.transforms),
341
+ generations,
336
342
  count,
337
343
  capacity: nextCapacity(Math.max(INITIAL_CAPACITY, count)),
338
344
  revision: 1,
@@ -348,13 +354,26 @@ export class InstanceCollectionStore {
348
354
  replace(
349
355
  id: InstanceCollectionId,
350
356
  transforms: Float32Array,
357
+ /** Prior identity generations in the replacement order; omitted rows are new. */
358
+ generations?: Uint32Array,
351
359
  ): InstanceCollectionResult<InstanceCollectionInfo> {
352
360
  const record = this.live('replace', id);
353
361
  if (!record.ok) return record;
354
362
  const invalidPayload = validateTransforms('replace', transforms, id);
355
363
  if (invalidPayload !== undefined) return err(invalidPayload);
356
364
  const count = transforms.length / MATRIX_STRIDE;
365
+ const generationResult =
366
+ generations === undefined
367
+ ? ok(this.allocateGenerations(count))
368
+ : this.validateReplacementGenerations(generations, count, id);
369
+ if (!generationResult.ok) return generationResult;
370
+ // Replacement is a topology boundary by default. A producer that keeps
371
+ // logical instance identity through compaction/reorder may pass the prior
372
+ // generation tokens in their new order; the record stage then maps the
373
+ // real previous transform by token instead of by draw ordinal.
374
+ const replacementGenerations = generationResult.value;
357
375
  record.value.transforms = new Float32Array(transforms);
376
+ record.value.generations = replacementGenerations;
358
377
  record.value.count = count;
359
378
  record.value.capacity = Math.max(record.value.capacity, nextCapacity(Math.max(1, count)));
360
379
  record.value.revision += 1;
@@ -402,6 +421,11 @@ export class InstanceCollectionStore {
402
421
  record.value.dirtyRanges = [];
403
422
  }
404
423
  record.value.transforms.set(input.transforms, input.start * MATRIX_STRIDE);
424
+ // An update changes a pose while retaining the same ordinal identity.
425
+ // Keep its generation stable so the record stage can pair this current
426
+ // transform with the last submitted transform and expose real velocity.
427
+ // `replace` is the explicit topology boundary that advances generations
428
+ // for compaction, reorder, retirement, and newly occupied ordinals.
405
429
  record.value.revision += 1;
406
430
  appendDirtyRange(record.value, input.start, input.start + span);
407
431
  this.epochValue += 1;
@@ -502,7 +526,12 @@ export class InstanceCollectionStore {
502
526
  if (!record.ok) return record;
503
527
  const value = record.value;
504
528
  value.lastSnapshotRevision = value.revision;
505
- const result = this.detachedSnapshot(value);
529
+ const result = {
530
+ ...this.info(value),
531
+ transforms: new Float32Array(value.transforms),
532
+ generations: new Uint32Array(value.generations),
533
+ dirtyRanges: value.dirtyRanges.map((range) => ({ ...range })),
534
+ };
506
535
  // Keep the current ranges stable for repeated reads by entities sharing
507
536
  // this collection. The next authoring mutation starts a fresh interval
508
537
  // set once this revision has been observed.
@@ -579,6 +608,7 @@ export class InstanceCollectionStore {
579
608
  return {
580
609
  ...this.info(record),
581
610
  transforms: new Float32Array(record.transforms),
611
+ generations: new Uint32Array(record.generations),
582
612
  dirtyRanges: record.dirtyRanges.map((range) => ({ ...range })),
583
613
  };
584
614
  }
@@ -592,6 +622,55 @@ export class InstanceCollectionStore {
592
622
  });
593
623
  }
594
624
 
625
+ private allocateGenerations(count: number): Uint32Array {
626
+ const generations = new Uint32Array(count);
627
+ for (let index = 0; index < count; index += 1) {
628
+ generations[index] = this.nextInstanceGeneration;
629
+ this.nextInstanceGeneration = (this.nextInstanceGeneration + 1) >>> 0 || 1;
630
+ }
631
+ return generations;
632
+ }
633
+
634
+ private validateReplacementGenerations(
635
+ generations: Uint32Array,
636
+ count: number,
637
+ collectionId: InstanceCollectionId,
638
+ ): InstanceCollectionResult<Uint32Array> {
639
+ if (!(generations instanceof Uint32Array) || generations.length !== count) {
640
+ return err(
641
+ invalid(
642
+ 'replace',
643
+ `generations is a Uint32Array with ${count} entries`,
644
+ generations instanceof Uint32Array ? generations.length : typeof generations,
645
+ collectionId,
646
+ { requestedBytes: generations?.byteLength ?? 0, cause: 'invalid-generation-shape' },
647
+ ),
648
+ );
649
+ }
650
+ const seen = new Set<number>();
651
+ for (const generation of generations) {
652
+ if (generation === 0 || seen.has(generation)) {
653
+ return err(
654
+ invalid(
655
+ 'replace',
656
+ 'generations contain unique non-zero identity tokens',
657
+ generation,
658
+ collectionId,
659
+ {
660
+ requestedBytes: generations.byteLength,
661
+ cause: 'invalid-generation-identity',
662
+ },
663
+ ),
664
+ );
665
+ }
666
+ seen.add(generation);
667
+ if (generation >= this.nextInstanceGeneration) {
668
+ this.nextInstanceGeneration = (generation + 1) >>> 0 || 1;
669
+ }
670
+ }
671
+ return ok(new Uint32Array(generations));
672
+ }
673
+
595
674
  private projectInspection(record: CollectionRecord): InstanceCollectionInspection {
596
675
  const residency = this.residency.get(record.id);
597
676
  return Object.freeze({