@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.
- package/README.md +379 -33
- package/dist/assembly/analytic-fog-registration.d.ts +3 -0
- package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
- package/dist/assembly/fxaa-registration.d.ts +4 -0
- package/dist/assembly/fxaa-registration.d.ts.map +1 -0
- package/dist/assembly/lod-observation.d.ts +4 -0
- package/dist/assembly/lod-observation.d.ts.map +1 -0
- package/dist/assembly/motion-blur-registration.d.ts +3 -0
- package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
- package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/bloom-admission.d.ts +13 -0
- package/dist/bloom-admission.d.ts.map +1 -0
- package/dist/{chunk-DRR7D3KX.mjs → chunk-3RTCJTH4.mjs} +12 -9
- package/dist/chunk-3RTCJTH4.mjs.map +1 -0
- package/dist/{chunk-S665NJNF.mjs → chunk-LEAXOMYJ.mjs} +3 -3
- package/dist/{chunk-S665NJNF.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
- package/dist/{chunk-6TFBXLRJ.mjs → chunk-NB2S7ZRX.mjs} +155 -11
- package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
- package/dist/{chunk-LULCA3OF.mjs → chunk-NNQL7CB3.mjs} +4 -4
- package/dist/{chunk-LULCA3OF.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
- package/dist/{chunk-4NNZQCL3.mjs → chunk-OO3DRYK7.mjs} +66 -200
- package/dist/chunk-OO3DRYK7.mjs.map +1 -0
- package/dist/{chunk-GWCEMDVR.mjs → chunk-PLAXOOX5.mjs} +232 -7
- package/dist/chunk-PLAXOOX5.mjs.map +1 -0
- package/dist/{chunk-JRIPCMYC.mjs → chunk-VOZWDM3L.mjs} +283 -193
- package/dist/chunk-VOZWDM3L.mjs.map +1 -0
- package/dist/{chunk-JCIRYQ4V.mjs → chunk-ZDQWGQW7.mjs} +10 -4
- package/dist/chunk-ZDQWGQW7.mjs.map +1 -0
- package/dist/components/atmosphere.d.ts +2 -0
- package/dist/components/atmosphere.d.ts.map +1 -1
- package/dist/components/camera.d.ts +20 -4
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/mesh-filter.d.ts +6 -5
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/motion-blur.d.ts +1 -0
- package/dist/components/motion-blur.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +2865 -526
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +6 -0
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/analytic-fog.d.ts +6 -0
- package/dist/environment/analytic-fog.d.ts.map +1 -0
- package/dist/errors/render.d.ts +6 -2
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +7 -0
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/extract/environment.d.ts +2 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/features/error-reporting.d.ts +13 -0
- package/dist/features/error-reporting.d.ts.map +1 -0
- package/dist/features/host.d.ts +5 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
- package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
- package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
- package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
- package/dist/features/plan.d.ts +7 -4
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +9 -5
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +7 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +3 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/view-gpu.d.ts +9 -0
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +6 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/inspection-types.d.ts +43 -2
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances.d.ts +8 -1
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +6 -6
- package/dist/pipeline/standard-post.d.ts +28 -19
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +2 -2
- package/dist/record/frame-snapshot.d.ts +3 -0
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +5 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +6 -5
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +12 -28
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +5 -3
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +24 -4
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +3 -1
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +8 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +7 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +2 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +7 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/temporal/view.d.ts +6 -1
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/typed-render-graph-primitives.d.ts +10 -7
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +0 -1
- package/dist/volume/component.d.ts.map +1 -1
- package/dist/volume/passes.d.ts.map +1 -1
- package/dist/volume/temporal.d.ts +3 -1
- package/dist/volume/temporal.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
- package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
- package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
- package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
- package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
- package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
- package/src/__tests__/environment-selection.unit.test.ts +12 -0
- package/src/__tests__/factory-contract.integration.test.ts +331 -68
- package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
- package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
- package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
- package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
- package/src/__tests__/gpu-scene.unit.test.ts +91 -0
- package/src/__tests__/instances-store.unit.test.ts +75 -0
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
- package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
- package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
- package/src/__tests__/public-surface.test-d.ts +8 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
- package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
- package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
- package/src/__tests__/shader-manifest-fixture.ts +4 -4
- package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
- package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
- package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
- package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
- package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
- package/src/__tests__/temporal-view.unit.test.ts +2 -1
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
- package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
- package/src/assembly/analytic-fog-registration.ts +14 -0
- package/src/assembly/fxaa-registration.ts +17 -0
- package/src/assembly/lod-observation.ts +12 -0
- package/src/assembly/motion-blur-registration.ts +17 -0
- package/src/assembly/renderer-facade.ts +2 -0
- package/src/assembly/temporal-post-process-prewarm.ts +40 -0
- package/src/assembly/webgpu-ready-contract.ts +8 -3
- package/src/assembly/webgpu-ready.ts +181 -245
- package/src/assembly/webgpu-renderer.ts +16 -31
- package/src/bloom-admission.ts +21 -0
- package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
- package/src/components/atmosphere.ts +4 -0
- package/src/components/camera.ts +66 -10
- package/src/components/mesh-filter.ts +11 -9
- package/src/components/motion-blur.ts +1 -0
- package/src/device/gpu-residency.ts +56 -12
- package/src/environment/analytic-fog.ts +44 -0
- package/src/environment/background.ts +2 -2
- package/src/errors/render.ts +14 -1
- package/src/extract/camera.ts +20 -7
- package/src/extract/contracts.ts +7 -0
- package/src/extract/environment.ts +4 -0
- package/src/features/error-reporting.ts +78 -0
- package/src/features/host.ts +25 -12
- package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
- package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
- package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
- package/src/features/motion-blur/motion-blur-params.ts +70 -2
- package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
- package/src/features/plan.ts +25 -8
- package/src/features/prepared-gpu-work.ts +86 -35
- package/src/features/render-graph-compute.ts +13 -5
- package/src/features/render-graph-contribution.ts +2 -0
- package/src/gpu-driven/material-artifacts.ts +2 -7
- package/src/gpu-driven/prepared-draw.ts +15 -10
- package/src/gpu-driven/production-raster.ts +83 -7
- package/src/gpu-driven/view-gpu.ts +17 -0
- package/src/gpu-scene.ts +188 -21
- package/src/index.ts +32 -0
- package/src/inspection-types.ts +60 -2
- package/src/instances.ts +80 -1
- package/src/pipeline/standard-post.ts +307 -78
- package/src/pipeline-spec.ts +5 -5
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
- package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
- package/src/record/frame-snapshot.ts +5 -1
- package/src/record/frame-targets.ts +10 -6
- package/src/record/frame.ts +163 -22
- package/src/record/main-pass-geometry.ts +30 -1
- package/src/record/main-pass-sprite-draws.ts +6 -1
- package/src/record/mesh-ssbo.ts +43 -6
- package/src/record/render-context.ts +12 -28
- package/src/record/shadow-pass.ts +6 -1
- package/src/record/skybox-post-pass.ts +122 -196
- package/src/record/typed-frame-graph.ts +153 -9
- package/src/recovery/render-system-candidate.ts +22 -10
- package/src/render-contract.ts +23 -4
- package/src/render-pipeline.ts +6 -1
- package/src/render-system-extract-tail.ts +16 -0
- package/src/render-system-extract.ts +45 -13
- package/src/render-system.ts +255 -120
- package/src/scene/render-scene.ts +85 -4
- package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
- package/src/temporal/frame-coordinator.ts +8 -1
- package/src/temporal/frame.ts +20 -0
- package/src/temporal/view.ts +13 -2
- package/src/typed-render-graph-primitives.ts +121 -48
- package/src/volume/component.ts +0 -1
- package/src/volume/passes.ts +8 -4
- package/src/volume/resources.ts +1 -1
- package/src/volume/temporal.ts +11 -0
- package/dist/chunk-4NNZQCL3.mjs.map +0 -1
- package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
- package/dist/chunk-DRR7D3KX.mjs.map +0 -1
- package/dist/chunk-GWCEMDVR.mjs.map +0 -1
- package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
- 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
|
+
});
|