@forgeax/engine-render 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -10
- package/dist/assembly/bundler-contract.d.ts +9 -0
- package/dist/assembly/bundler-contract.d.ts.map +1 -0
- package/dist/assembly/device-feature-admission.d.ts +8 -0
- package/dist/assembly/device-feature-admission.d.ts.map +1 -1
- package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
- package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +7 -243
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +3 -2
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
- package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
- package/dist/assembly/material/surface-projection.d.ts +24 -0
- package/dist/assembly/material/surface-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +188 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -0
- package/dist/assembly/material-shader-prewarm.d.ts +21 -0
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
- package/dist/assembly/recovery/generation.d.ts +46 -0
- package/dist/assembly/recovery/generation.d.ts.map +1 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
- package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts +10 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -0
- package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-helpers.d.ts +19 -0
- package/dist/assembly/renderer-helpers.d.ts.map +1 -0
- package/dist/assembly/renderer-lifecycle.d.ts +34 -0
- package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
- package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
- package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +55 -0
- package/dist/assembly/webgpu-ready.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +152 -0
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
- package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
- package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
- package/dist/assets/asset-decoders.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-FF7K3FSO.mjs → chunk-ADHHVYLW.mjs} +89 -89
- package/dist/chunk-ADHHVYLW.mjs.map +1 -0
- package/dist/{chunk-YALPS44A.mjs → chunk-E34VL5VI.mjs} +47 -47
- package/dist/chunk-E34VL5VI.mjs.map +1 -0
- package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
- package/dist/chunk-GE3SDD72.mjs.map +1 -0
- package/dist/{chunk-HZUPSYW7.mjs → chunk-HKXTW355.mjs} +105 -15
- package/dist/chunk-HKXTW355.mjs.map +1 -0
- package/dist/chunk-JSEDTZYS.mjs +413 -0
- package/dist/chunk-JSEDTZYS.mjs.map +1 -0
- package/dist/{chunk-FBC4R44J.mjs → chunk-KM2NOX2I.mjs} +57 -57
- package/dist/chunk-KM2NOX2I.mjs.map +1 -0
- package/dist/{chunk-DIYW3M5D.mjs → chunk-N3RT2EUR.mjs} +2288 -2238
- package/dist/chunk-N3RT2EUR.mjs.map +1 -0
- package/dist/{chunk-ZUWKAZ2I.mjs → chunk-TCLDX7SZ.mjs} +1167 -368
- package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
- package/dist/{chunk-QODA5UKU.mjs → chunk-ZZ4YQ474.mjs} +2 -2
- package/dist/chunk-ZZ4YQ474.mjs.map +1 -0
- package/dist/components/instances.d.ts +13 -31
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +20688 -16314
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/device-scope.d.ts +2 -0
- package/dist/device/device-scope.d.ts.map +1 -1
- package/dist/device/gpu-residency.d.ts +24 -4
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/frame.d.ts +5 -0
- package/dist/environment/frame.d.ts.map +1 -1
- package/dist/environment/inspection.d.ts +7 -0
- package/dist/environment/inspection.d.ts.map +1 -1
- package/dist/environment/lifecycle.d.ts +20 -2
- package/dist/environment/lifecycle.d.ts.map +1 -1
- package/dist/errors/recover.d.ts +58 -2
- package/dist/errors/recover.d.ts.map +1 -1
- package/dist/errors/render.d.ts +13 -0
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +899 -0
- package/dist/extract/contracts.d.ts.map +1 -0
- package/dist/features/host.d.ts +2 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +3 -0
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/prepared-graphics-store.d.ts +2 -0
- package/dist/features/prepared-graphics-store.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +34 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +5 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +7 -6
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +7 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +27 -9
- package/dist/index.mjs.map +1 -1
- package/dist/instance-buffer-cache.d.ts +7 -1
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +101 -0
- package/dist/instances.d.ts.map +1 -0
- package/dist/internal.mjs +6 -6
- package/dist/light-buffer-layout.d.ts +3 -0
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/materials.d.ts +96 -11
- package/dist/materials.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +56 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/occlusion-proxy.d.ts +4 -0
- package/dist/occlusion-proxy.d.ts.map +1 -0
- package/dist/pbr-pipeline.d.ts +32 -9
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/points-lines/prepare.d.ts +3 -0
- package/dist/points-lines/prepare.d.ts.map +1 -1
- package/dist/points-lines/standard-owner.d.ts +33 -0
- package/dist/points-lines/standard-owner.d.ts.map +1 -0
- package/dist/record/frame-snapshot.d.ts +18 -2
- 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 +14 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +36 -4
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +7 -17
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +11 -3
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/recovery-pipeline.d.ts +30 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -0
- package/dist/record/render-context.d.ts +28 -5
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +11 -1
- package/dist/record/shadow-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 +65 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -0
- package/dist/render-contract.d.ts +68 -7
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +16 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -0
- package/dist/render-system-extract.d.ts +180 -22
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +131 -4
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +2 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +31 -1
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/systems/skin-palette-allocator.d.ts +6 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +6 -6
- package/dist/temporal/taa-history-store.d.ts +15 -1
- package/dist/temporal/taa-history-store.d.ts.map +1 -1
- package/dist/temporal/transaction.d.ts +5 -0
- package/dist/temporal/transaction.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
- package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
- package/src/__tests__/environment-generation.integration.test.ts +23 -0
- package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
- package/src/__tests__/factory-contract.integration.test.ts +122 -7
- package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
- package/src/__tests__/gpu-scene.unit.test.ts +38 -2
- package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
- package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
- package/src/__tests__/instances-store.unit.test.ts +46 -0
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +29 -22
- package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
- package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
- package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
- package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
- package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
- package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -5
- package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
- package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
- package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +127 -18
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
- package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
- package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
- package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
- package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
- package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
- package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
- package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
- package/src/__tests__/shader-manifest-fixture.ts +13 -16
- package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
- package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
- package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
- package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
- package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
- package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
- package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
- package/src/assembly/bundler-contract.ts +9 -0
- package/src/assembly/device-feature-admission.ts +15 -5
- package/src/assembly/extended-lighting-fallback.ts +216 -0
- package/src/assembly/factory.ts +83 -10088
- package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
- package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
- package/src/assembly/material/assembly.ts +18 -2
- package/src/assembly/material/standard-layer-projection.ts +16 -0
- package/src/assembly/material/surface-projection.ts +90 -0
- package/src/assembly/material-shader-policy.ts +932 -0
- package/src/assembly/material-shader-prewarm.ts +88 -0
- package/src/assembly/mesh-ssbo-grow.ts +270 -0
- package/src/assembly/recovery/device-loss-fanout.ts +89 -0
- package/src/assembly/recovery/generation.ts +141 -0
- package/src/assembly/recovery/recovery-attempt.ts +800 -0
- package/src/assembly/recovery/renderer-recover.ts +906 -0
- package/src/assembly/renderer-facade.ts +342 -0
- package/src/assembly/renderer-frame-transaction.ts +45 -0
- package/src/assembly/renderer-helpers.ts +99 -0
- package/src/assembly/renderer-lifecycle.ts +159 -0
- package/src/assembly/shader-prewarm-policy.ts +157 -0
- package/src/assembly/webgpu-ready-contract.ts +14 -0
- package/src/assembly/webgpu-ready.ts +4066 -0
- package/src/assembly/webgpu-renderer.ts +4027 -0
- package/src/assembly/webgpu-vertex-layouts.ts +132 -0
- package/src/assets/asset-decoders.ts +27 -4
- package/src/components/camera.ts +1 -1
- package/src/components/instances.ts +14 -197
- package/src/debug-draw-glue.ts +1 -1
- package/src/device/device-scope.ts +35 -6
- package/src/device/gpu-residency.ts +194 -26
- package/src/environment/frame.ts +6 -0
- package/src/environment/inspection.ts +10 -0
- package/src/environment/lifecycle.ts +114 -8
- package/src/errors/recover.ts +76 -5
- package/src/errors/render.ts +17 -0
- package/src/extract/contracts.ts +974 -0
- package/src/features/host.ts +16 -0
- package/src/features/prepared-gpu-work.ts +20 -2
- package/src/features/prepared-graphics-store.ts +17 -0
- package/src/gpu-driven/production-raster.ts +131 -3
- package/src/gpu-scene.ts +87 -31
- package/src/ibl/IblPipelineCache.ts +134 -50
- package/src/ibl/skylight-bind-group.ts +26 -0
- package/src/index.ts +10 -1
- package/src/instance-buffer-cache.ts +14 -1
- package/src/instances.ts +288 -0
- package/src/light-buffer-layout.ts +3 -0
- package/src/materials.ts +365 -53
- package/src/mesh-material-bindings.ts +59 -0
- package/src/occlusion-proxy.ts +64 -0
- package/src/pbr-pipeline.ts +139 -15
- package/src/pipeline-builder.ts +14 -10
- package/src/pipeline-spec.ts +6 -14
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
- package/src/points-lines/prepare.ts +9 -0
- package/src/points-lines/standard-owner.ts +403 -0
- package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
- package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
- package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
- package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
- package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
- package/src/record/frame-snapshot.ts +34 -2
- package/src/record/frame-targets.ts +11 -1
- package/src/record/frame.ts +37 -4
- package/src/record/helpers.ts +1 -1
- package/src/record/main-pass-geometry.ts +727 -63
- package/src/record/main-pass-material.ts +316 -209
- package/src/record/main-pass-sprite-draws.ts +7 -8
- package/src/record/main-pass.ts +94 -6
- package/src/record/mesh-ssbo.ts +32 -10
- package/src/record/recovery-pipeline.ts +695 -0
- package/src/record/render-context.ts +39 -4
- package/src/record/shadow-pass.ts +46 -99
- package/src/record/typed-frame-graph.ts +45 -1
- package/src/record/view-ubo.ts +3 -3
- package/src/recovery/render-system-candidate.ts +876 -0
- package/src/render-contract.ts +69 -7
- package/src/render-system-extract-tail.ts +3039 -0
- package/src/render-system-extract.ts +177 -3003
- package/src/render-system.ts +906 -662
- package/src/scene/render-scene-types.ts +2 -0
- package/src/scene/render-scene.ts +703 -37
- package/src/systems/skin-palette-allocator.ts +129 -7
- package/src/temporal/frame-coordinator.ts +4 -1
- package/src/temporal/taa-history-store.ts +46 -2
- package/src/temporal/temporal-view.ts +1 -1
- package/src/temporal/transaction.ts +9 -0
- package/src/tilemap-chunk-extract-system.ts +1 -1
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
- package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
- package/dist/chunk-4L3P6DT5.mjs.map +0 -1
- package/dist/chunk-4NPUASZR.mjs +0 -203
- package/dist/chunk-4NPUASZR.mjs.map +0 -1
- package/dist/chunk-DIYW3M5D.mjs.map +0 -1
- package/dist/chunk-FBC4R44J.mjs.map +0 -1
- package/dist/chunk-FF7K3FSO.mjs.map +0 -1
- package/dist/chunk-HZUPSYW7.mjs.map +0 -1
- package/dist/chunk-QODA5UKU.mjs.map +0 -1
- package/dist/chunk-YALPS44A.mjs.map +0 -1
- package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { RhiDevice } from '@forgeax/engine-rhi';
|
|
2
|
+
import { rhi } from '@forgeax/engine-rhi-null';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { attachDeviceLostFanout } from '../assembly/recovery/device-loss-fanout';
|
|
5
|
+
import {
|
|
6
|
+
HealthListenerRegistry,
|
|
7
|
+
LostListenerRegistry,
|
|
8
|
+
RhiErrorListenerRegistry,
|
|
9
|
+
} from '../lifecycle';
|
|
10
|
+
|
|
11
|
+
function deferred<T>(): {
|
|
12
|
+
readonly promise: Promise<T>;
|
|
13
|
+
readonly resolve: (value: T) => void;
|
|
14
|
+
} {
|
|
15
|
+
let resolve!: (value: T) => void;
|
|
16
|
+
const promise = new Promise<T>((next) => {
|
|
17
|
+
resolve = next;
|
|
18
|
+
});
|
|
19
|
+
return { promise, resolve };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('device loss fan-out generation fence', () => {
|
|
23
|
+
it('counts a late old-generation notification without changing active health', async () => {
|
|
24
|
+
const lost = deferred<{ readonly reason: 'unknown'; readonly message: string }>();
|
|
25
|
+
const lostRegistry = new LostListenerRegistry();
|
|
26
|
+
const errorRegistry = new RhiErrorListenerRegistry();
|
|
27
|
+
const healthRegistry = new HealthListenerRegistry();
|
|
28
|
+
const currentGeneration = 2;
|
|
29
|
+
let staleLossEvents = 0;
|
|
30
|
+
let lostEvents = 0;
|
|
31
|
+
let healthEvents = 0;
|
|
32
|
+
let errorEvents = 0;
|
|
33
|
+
const device = { lost: lost.promise } as unknown as RhiDevice;
|
|
34
|
+
lostRegistry.add(() => {
|
|
35
|
+
lostEvents += 1;
|
|
36
|
+
});
|
|
37
|
+
healthRegistry.add(() => {
|
|
38
|
+
healthEvents += 1;
|
|
39
|
+
});
|
|
40
|
+
errorRegistry.add(() => {
|
|
41
|
+
errorEvents += 1;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
attachDeviceLostFanout(
|
|
45
|
+
device,
|
|
46
|
+
{ rhi },
|
|
47
|
+
{
|
|
48
|
+
lostRegistry,
|
|
49
|
+
errorRegistry,
|
|
50
|
+
healthRegistry,
|
|
51
|
+
generation: 1,
|
|
52
|
+
currentGeneration: () => currentGeneration,
|
|
53
|
+
onStaleLoss: () => {
|
|
54
|
+
staleLossEvents += 1;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
lost.resolve({ reason: 'unknown', message: 'late retired-device notification' });
|
|
60
|
+
await Promise.resolve();
|
|
61
|
+
await Promise.resolve();
|
|
62
|
+
|
|
63
|
+
expect(currentGeneration).toBe(2);
|
|
64
|
+
expect(staleLossEvents).toBe(1);
|
|
65
|
+
expect(lostEvents).toBe(0);
|
|
66
|
+
expect(healthEvents).toBe(0);
|
|
67
|
+
expect(errorEvents).toBe(0);
|
|
68
|
+
expect(healthRegistry.getLastSnapshot().reason).toBe('alive');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('uses the backend loss projection for deterministic recovery fixtures', async () => {
|
|
72
|
+
const native = deferred<{ readonly reason: 'unknown'; readonly message: string }>();
|
|
73
|
+
const projected = deferred<{ readonly reason: 'unknown'; readonly message: string }>();
|
|
74
|
+
const lostRegistry = new LostListenerRegistry();
|
|
75
|
+
const errorRegistry = new RhiErrorListenerRegistry();
|
|
76
|
+
const healthRegistry = new HealthListenerRegistry();
|
|
77
|
+
let lostEvents = 0;
|
|
78
|
+
lostRegistry.add(() => {
|
|
79
|
+
lostEvents += 1;
|
|
80
|
+
});
|
|
81
|
+
const device = { lost: native.promise } as unknown as RhiDevice;
|
|
82
|
+
|
|
83
|
+
attachDeviceLostFanout(
|
|
84
|
+
device,
|
|
85
|
+
{
|
|
86
|
+
rhi,
|
|
87
|
+
instrumentation: {
|
|
88
|
+
deviceLost: () => projected.promise,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
lostRegistry,
|
|
93
|
+
errorRegistry,
|
|
94
|
+
healthRegistry,
|
|
95
|
+
generation: 0,
|
|
96
|
+
currentGeneration: () => 0,
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
projected.resolve({ reason: 'unknown', message: 'fixture-projected loss' });
|
|
101
|
+
await Promise.resolve();
|
|
102
|
+
await Promise.resolve();
|
|
103
|
+
|
|
104
|
+
expect(lostEvents).toBe(1);
|
|
105
|
+
expect(healthRegistry.getLastSnapshot().reason).toBe('device-lost');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -148,4 +148,25 @@ describe('DeviceScope lifecycle transaction', () => {
|
|
|
148
148
|
expect(scope.state).toBe('disposed');
|
|
149
149
|
expect(scope.resourceDelta()).toBe(0);
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
it('runs an adopted cleanup once across repeated scope termination', async () => {
|
|
153
|
+
const scope = DeviceScope.create(31, 'renderer');
|
|
154
|
+
let cleanupCount = 0;
|
|
155
|
+
const transaction = new LifecycleTransaction(scope);
|
|
156
|
+
transaction.add({
|
|
157
|
+
kind: 'pipeline',
|
|
158
|
+
create: () => ({ id: 'candidate-pipeline' }),
|
|
159
|
+
cleanup: () => {
|
|
160
|
+
cleanupCount += 1;
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = await transaction.commit();
|
|
165
|
+
|
|
166
|
+
expect(result.ok).toBe(true);
|
|
167
|
+
scope.retire();
|
|
168
|
+
scope.dispose();
|
|
169
|
+
scope.abandon();
|
|
170
|
+
expect(cleanupCount).toBe(1);
|
|
171
|
+
});
|
|
151
172
|
});
|
|
@@ -154,4 +154,27 @@ describe('Environment generation candidate and LKG lifecycle (M2)', () => {
|
|
|
154
154
|
expect(recovered.value.generation).not.toBe(old.value.generation);
|
|
155
155
|
expect(recovered.value.liveHandle).not.toBe(old.value.liveHandle);
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
it('prepares a recovery root on a detached lifecycle without changing the active owner', async () => {
|
|
159
|
+
const active = new EnvironmentLifecycle(DeviceScope.create(20, 'environment-active'));
|
|
160
|
+
const initial = await active.ensure(frame('sky-active'));
|
|
161
|
+
expect(initial.ok).toBe(true);
|
|
162
|
+
if (!initial.ok) return;
|
|
163
|
+
active.publish(initial.value);
|
|
164
|
+
active.resetForRecover(21);
|
|
165
|
+
const before = active.inspect();
|
|
166
|
+
|
|
167
|
+
const candidateScope = DeviceScope.create(21, 'environment-candidate');
|
|
168
|
+
const candidate = active.createRecoveryCandidate(candidateScope);
|
|
169
|
+
const root = candidate.createRecoveryRoot(candidateScope);
|
|
170
|
+
const recovered =
|
|
171
|
+
(await root.create()) as import('../environment/generation').EnvironmentGeneration;
|
|
172
|
+
|
|
173
|
+
expect(active.inspect()).toEqual(before);
|
|
174
|
+
expect(candidate.inspect().candidate?.generation).toBe(recovered.generation);
|
|
175
|
+
|
|
176
|
+
await root.cleanup(recovered);
|
|
177
|
+
expect(candidate.inspect().candidate).toBeUndefined();
|
|
178
|
+
expect(active.inspect()).toEqual(before);
|
|
179
|
+
});
|
|
157
180
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { err } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { exposeRenderer } from '../assembly/factory';
|
|
4
|
+
import type { RendererHostImplementation } from '../assembly/host-contract';
|
|
5
|
+
import { RendererContractFailureError } from '../errors/render';
|
|
6
|
+
|
|
7
|
+
describe('exposeRenderer error projection', () => {
|
|
8
|
+
it('reports an internal draw contract failure as a device operation failure', () => {
|
|
9
|
+
const host = {
|
|
10
|
+
inspect: () => ({ state: 'alive' }),
|
|
11
|
+
subscribeHostEvents: () => () => undefined,
|
|
12
|
+
drawFrame: () =>
|
|
13
|
+
err(
|
|
14
|
+
new RendererContractFailureError(
|
|
15
|
+
'draw',
|
|
16
|
+
'the Standard render owner did not submit a command buffer',
|
|
17
|
+
),
|
|
18
|
+
),
|
|
19
|
+
} as unknown as RendererHostImplementation;
|
|
20
|
+
const renderer = exposeRenderer(host);
|
|
21
|
+
|
|
22
|
+
const result = renderer.draw({} as never);
|
|
23
|
+
|
|
24
|
+
expect(result.ok).toBe(false);
|
|
25
|
+
if (!result.ok) {
|
|
26
|
+
expect(result.error.code).toBe('device-operation-failed');
|
|
27
|
+
if (result.error.code !== 'device-operation-failed') return;
|
|
28
|
+
expect(result.error.detail.cause).toMatchObject({
|
|
29
|
+
code: 'renderer-contract-failed',
|
|
30
|
+
detail: { operation: 'draw' },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -32,6 +32,7 @@ import { renderLifecycleManifestUrl } from './shader-manifest-fixture';
|
|
|
32
32
|
|
|
33
33
|
const assemblyDirectory = dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
const factorySourcePath = resolve(assemblyDirectory, '../assembly/factory.ts');
|
|
35
|
+
const webgpuRendererSourcePath = resolve(assemblyDirectory, '../assembly/webgpu-renderer.ts');
|
|
35
36
|
const renderTargetHostSourcePath = resolve(assemblyDirectory, '../assembly/render-target-host.ts');
|
|
36
37
|
|
|
37
38
|
function manifestUrl(): string {
|
|
@@ -127,7 +128,7 @@ describe('factory contract', () => {
|
|
|
127
128
|
});
|
|
128
129
|
|
|
129
130
|
it('forwards the bundler build identity into RenderSystem inspections', () => {
|
|
130
|
-
const source = readFileSync(
|
|
131
|
+
const source = readFileSync(webgpuRendererSourcePath, 'utf8');
|
|
131
132
|
expect(source).toContain('get build() {\n return internals.bundler?.build;\n },');
|
|
132
133
|
});
|
|
133
134
|
|
|
@@ -1700,6 +1701,15 @@ describe('factory contract', () => {
|
|
|
1700
1701
|
expect((await renderer.initialization).ok).toBe(true);
|
|
1701
1702
|
expect(world.update().ok).toBe(true);
|
|
1702
1703
|
expect(renderer.draw(input()).ok).toBe(true);
|
|
1704
|
+
expect(renderer.inspect().recovery).toMatchObject({
|
|
1705
|
+
phase: null,
|
|
1706
|
+
fromGeneration: 0,
|
|
1707
|
+
candidateGeneration: 0,
|
|
1708
|
+
attempt: 0,
|
|
1709
|
+
lastOutcome: 'none',
|
|
1710
|
+
rehydratedRoots: 0,
|
|
1711
|
+
staleLossEvents: 0,
|
|
1712
|
+
});
|
|
1703
1713
|
const beforeLoss = renderer.inspect().bloom;
|
|
1704
1714
|
const beforeDeviceGeneration = renderer.inspect().temporal.deviceGeneration;
|
|
1705
1715
|
expect(beforeLoss.state).toBe('active');
|
|
@@ -1710,15 +1720,34 @@ describe('factory contract', () => {
|
|
|
1710
1720
|
const failed = await renderer.recover();
|
|
1711
1721
|
expect(failed.ok).toBe(false);
|
|
1712
1722
|
expect(renderer.health().reason).toBe('device-lost');
|
|
1723
|
+
expect(renderer.inspect().recovery).toMatchObject({
|
|
1724
|
+
phase: null,
|
|
1725
|
+
fromGeneration: 0,
|
|
1726
|
+
candidateGeneration: 1,
|
|
1727
|
+
attempt: 1,
|
|
1728
|
+
lastOutcome: 'failed',
|
|
1729
|
+
rehydratedRoots: 0,
|
|
1730
|
+
staleLossEvents: 0,
|
|
1731
|
+
});
|
|
1713
1732
|
expect(renderer.inspect().perFramePassNames).toEqual([]);
|
|
1714
1733
|
expect(renderer.inspect().standardLighting).toBeUndefined();
|
|
1715
1734
|
const recovered = await renderer.recover();
|
|
1716
1735
|
expect(recovered.ok).toBe(true);
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1736
|
+
expect(renderer.inspect().recovery).toMatchObject({
|
|
1737
|
+
phase: null,
|
|
1738
|
+
fromGeneration: 0,
|
|
1739
|
+
candidateGeneration: 1,
|
|
1740
|
+
attempt: 2,
|
|
1741
|
+
lastOutcome: 'succeeded',
|
|
1742
|
+
rehydratedRoots: expect.any(Number),
|
|
1743
|
+
staleLossEvents: 0,
|
|
1744
|
+
});
|
|
1745
|
+
expect(renderer.inspect().recovery.rehydratedRoots).toBeGreaterThan(0);
|
|
1746
|
+
// Recovery publishes only after the retained CPU/LKG frame has prepared
|
|
1747
|
+
// the replacement graph and visible residency. The first post-recovery
|
|
1748
|
+
// draw therefore starts with graph accessors and Standard inspection.
|
|
1749
|
+
expect(renderer.inspect().perFramePassNames.length).toBeGreaterThan(0);
|
|
1750
|
+
expect(renderer.inspect().standardLighting).toBeDefined();
|
|
1722
1751
|
expect(world.update().ok).toBe(true);
|
|
1723
1752
|
expect(renderer.draw(input()).ok).toBe(true);
|
|
1724
1753
|
expect(renderer.inspect().perFramePassNames.length).toBeGreaterThan(0);
|
|
@@ -1868,7 +1897,11 @@ describe('factory contract', () => {
|
|
|
1868
1897
|
await Promise.resolve();
|
|
1869
1898
|
await Promise.resolve();
|
|
1870
1899
|
expect(renderer.health().reason).toBe('device-lost');
|
|
1871
|
-
|
|
1900
|
+
const failedRecovery = await renderer.recover();
|
|
1901
|
+
expect(failedRecovery.ok).toBe(false);
|
|
1902
|
+
if (!failedRecovery.ok) {
|
|
1903
|
+
expect(failedRecovery.error.code).toBe('recovery-failed');
|
|
1904
|
+
}
|
|
1872
1905
|
expect(renderer.health().reason).toBe('device-lost');
|
|
1873
1906
|
expect(createdReplacementHandles.size).toBeGreaterThan(0);
|
|
1874
1907
|
expect(
|
|
@@ -1907,4 +1940,86 @@ describe('factory contract', () => {
|
|
|
1907
1940
|
await renderer.dispose();
|
|
1908
1941
|
}
|
|
1909
1942
|
});
|
|
1943
|
+
|
|
1944
|
+
it('reports a structured disposed outcome when recovery is interrupted by dispose', async () => {
|
|
1945
|
+
const adapter = new RhiNullAdapter();
|
|
1946
|
+
const lost = deferred<{ readonly reason: 'unknown'; readonly message: string }>();
|
|
1947
|
+
const recoveryAdapterEntered = deferred<void>();
|
|
1948
|
+
const releaseRecoveryAdapter = deferred<void>();
|
|
1949
|
+
const firstDevice = (await adapter.requestDevice()).unwrap();
|
|
1950
|
+
const lostProxy = new Proxy(firstDevice, {
|
|
1951
|
+
get(target, property) {
|
|
1952
|
+
if (property === 'lost') return lost.promise;
|
|
1953
|
+
const value = Reflect.get(target, property, target);
|
|
1954
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
1955
|
+
},
|
|
1956
|
+
});
|
|
1957
|
+
let requestCount = 0;
|
|
1958
|
+
const recoverableRhi = {
|
|
1959
|
+
...rhi,
|
|
1960
|
+
requestAdapter: async () => {
|
|
1961
|
+
requestCount += 1;
|
|
1962
|
+
if (requestCount === 1) {
|
|
1963
|
+
return ok({
|
|
1964
|
+
features: adapter.features,
|
|
1965
|
+
limits: adapter.limits,
|
|
1966
|
+
requestDevice: async () => ok(lostProxy),
|
|
1967
|
+
} as never);
|
|
1968
|
+
}
|
|
1969
|
+
recoveryAdapterEntered.resolve();
|
|
1970
|
+
await releaseRecoveryAdapter.promise;
|
|
1971
|
+
return ok({
|
|
1972
|
+
features: adapter.features,
|
|
1973
|
+
limits: adapter.limits,
|
|
1974
|
+
requestDevice: async () => ok(firstDevice),
|
|
1975
|
+
} as never);
|
|
1976
|
+
},
|
|
1977
|
+
} as never;
|
|
1978
|
+
const renderer = await constructRenderer(
|
|
1979
|
+
{ width: 64, height: 64, getContext: () => null },
|
|
1980
|
+
{ rhi: recoverableRhi },
|
|
1981
|
+
{ shaderManifestUrl: renderLifecycleManifestUrl() },
|
|
1982
|
+
);
|
|
1983
|
+
|
|
1984
|
+
try {
|
|
1985
|
+
expect((await renderer.initialization).ok).toBe(true);
|
|
1986
|
+
lost.reject({ reason: 'unknown', message: 'forced device loss' });
|
|
1987
|
+
await Promise.resolve();
|
|
1988
|
+
await Promise.resolve();
|
|
1989
|
+
expect(renderer.health().reason).toBe('device-lost');
|
|
1990
|
+
|
|
1991
|
+
const recovery = renderer.recover();
|
|
1992
|
+
await recoveryAdapterEntered.promise;
|
|
1993
|
+
expect(renderer.inspect().recovery.phase).toBe('acquire-adapter');
|
|
1994
|
+
expect((await renderer.dispose()).ok).toBe(true);
|
|
1995
|
+
releaseRecoveryAdapter.resolve();
|
|
1996
|
+
|
|
1997
|
+
const result = await recovery;
|
|
1998
|
+
expect(result.ok).toBe(false);
|
|
1999
|
+
if (!result.ok) {
|
|
2000
|
+
if (result.error.code !== 'recovery-failed') {
|
|
2001
|
+
throw new Error(`Expected recovery-failed, received ${result.error.code}`);
|
|
2002
|
+
}
|
|
2003
|
+
expect(result.error.detail).toMatchObject({
|
|
2004
|
+
phase: 'cleanup',
|
|
2005
|
+
oldGeneration: 0,
|
|
2006
|
+
candidateGeneration: 1,
|
|
2007
|
+
attempt: 1,
|
|
2008
|
+
lastOutcome: 'disposed',
|
|
2009
|
+
rehydratedRoots: 0,
|
|
2010
|
+
staleLossEvents: 0,
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
expect(renderer.inspect().recovery).toMatchObject({
|
|
2014
|
+
phase: null,
|
|
2015
|
+
attempt: 1,
|
|
2016
|
+
lastOutcome: 'disposed',
|
|
2017
|
+
rehydratedRoots: 0,
|
|
2018
|
+
staleLossEvents: 0,
|
|
2019
|
+
});
|
|
2020
|
+
} finally {
|
|
2021
|
+
releaseRecoveryAdapter.resolve();
|
|
2022
|
+
await renderer.dispose();
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
1910
2025
|
});
|
|
@@ -3,7 +3,10 @@ import { describe, expect, it } from 'vitest';
|
|
|
3
3
|
import { FALLBACK_BYTES_PER_ROW } from '../ibl/skylight-bind-group';
|
|
4
4
|
|
|
5
5
|
const ownerSource = readFileSync(new URL('../ibl/skylight-bind-group.ts', import.meta.url), 'utf8');
|
|
6
|
-
const rendererSource = readFileSync(
|
|
6
|
+
const rendererSource = readFileSync(
|
|
7
|
+
new URL('../assembly/webgpu-ready.ts', import.meta.url),
|
|
8
|
+
'utf8',
|
|
9
|
+
);
|
|
7
10
|
|
|
8
11
|
describe('fallback texture row-stride owner', () => {
|
|
9
12
|
it('keeps both 1x1 fallback upload paths on one owner', () => {
|
|
@@ -120,9 +120,17 @@ describe('GpuScene', () => {
|
|
|
120
120
|
expect(created.scene.inspect().uploadBytes).toBe(bytesBefore);
|
|
121
121
|
|
|
122
122
|
created.scene.commitTemporalFrame(true).unwrap();
|
|
123
|
-
expect(queue.writes).toHaveLength(writesBefore
|
|
123
|
+
expect(queue.writes).toHaveLength(writesBefore);
|
|
124
|
+
expect(created.scene.inspect().uploadBytes).toBe(bytesBefore);
|
|
125
|
+
|
|
126
|
+
const dirty = projection.apply([
|
|
127
|
+
{ kind: 'update-transform', worldId: 0, entityKey: 0, world: snapshot(0, 3).transform.world },
|
|
128
|
+
]);
|
|
129
|
+
created.scene.sync(dirty).unwrap();
|
|
130
|
+
created.scene.commitTemporalFrame(true).unwrap();
|
|
131
|
+
expect(queue.writes).toHaveLength(writesBefore + 2);
|
|
124
132
|
expect(created.scene.inspect().uploadBytes).toBe(
|
|
125
|
-
bytesBefore +
|
|
133
|
+
bytesBefore + 2 * GPU_SCENE_LAYOUTS.transform.stride,
|
|
126
134
|
);
|
|
127
135
|
});
|
|
128
136
|
|
|
@@ -200,4 +208,32 @@ describe('GpuScene', () => {
|
|
|
200
208
|
gpuSceneFieldOffset(GPU_SCENE_LAYOUTS.transform, 'previousWorld') + 12 * 4;
|
|
201
209
|
expect(new DataView(transformWrite.bytes.buffer).getFloat32(previousTranslation, true)).toBe(9);
|
|
202
210
|
});
|
|
211
|
+
|
|
212
|
+
it('keeps a recycled slot allocated for the replacement identity', () => {
|
|
213
|
+
const queue = new RecordingQueue();
|
|
214
|
+
const created = GpuScene.create(createDevice(queue), 1).unwrap();
|
|
215
|
+
if (created.status !== 'available') return;
|
|
216
|
+
const projection = new RenderScene();
|
|
217
|
+
created.scene.sync(projection.apply([{ kind: 'create', snapshot: snapshot(1, 2) }])).unwrap();
|
|
218
|
+
|
|
219
|
+
const recycled = projection.apply([
|
|
220
|
+
{ kind: 'remove', worldId: 0, entityKey: 1 },
|
|
221
|
+
{ kind: 'create', snapshot: snapshot(2, 7) },
|
|
222
|
+
]);
|
|
223
|
+
expect(recycled.removedSlots[0]?.slot).toBe(recycled.createdSlots[0]?.slot);
|
|
224
|
+
created.scene.sync(recycled).unwrap();
|
|
225
|
+
|
|
226
|
+
expect(() =>
|
|
227
|
+
created.scene.sync(
|
|
228
|
+
projection.apply([
|
|
229
|
+
{
|
|
230
|
+
kind: 'update-transform',
|
|
231
|
+
worldId: 0,
|
|
232
|
+
entityKey: 2,
|
|
233
|
+
world: snapshot(2, 11).transform.world,
|
|
234
|
+
},
|
|
235
|
+
]),
|
|
236
|
+
),
|
|
237
|
+
).not.toThrow();
|
|
238
|
+
});
|
|
203
239
|
});
|
|
@@ -38,9 +38,11 @@ describe('IBL face uniform RHI contract', () => {
|
|
|
38
38
|
const badFace = writeFaceUniforms(device, bufferResult.value, 6, new Float32Array(16));
|
|
39
39
|
const badMatrix = writeFaceUniforms(device, bufferResult.value, 0, new Float32Array(15));
|
|
40
40
|
const badSubPass = writePrefilterUniforms(device, bufferResult.value, 30, 0.5, 128);
|
|
41
|
+
const badMipCount = writePrefilterUniforms(device, bufferResult.value, 0, 0.5, 128, 0);
|
|
41
42
|
|
|
42
43
|
expect(badFace).toMatchObject({ ok: false, error: { code: 'webgpu-runtime-error' } });
|
|
43
44
|
expect(badMatrix).toMatchObject({ ok: false, error: { code: 'webgpu-runtime-error' } });
|
|
44
45
|
expect(badSubPass).toMatchObject({ ok: false, error: { code: 'webgpu-runtime-error' } });
|
|
46
|
+
expect(badMipCount).toMatchObject({ ok: false, error: { code: 'webgpu-runtime-error' } });
|
|
45
47
|
});
|
|
46
48
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const source = readFileSync(new URL('../device/gpu-residency.ts', import.meta.url), 'utf8');
|
|
5
|
+
const iblSource = readFileSync(new URL('../ibl/IblPipelineCache.ts', import.meta.url), 'utf8');
|
|
6
|
+
|
|
7
|
+
describe('IBL residency publication', () => {
|
|
8
|
+
it('publishes ready and the idempotent cube only after submission succeeds', () => {
|
|
9
|
+
const precomputeCall = source.indexOf('const runResult = await runIblPrecompute({');
|
|
10
|
+
const precomputeSuccess = source.indexOf('if (!runResult.ok)', precomputeCall);
|
|
11
|
+
const sourceReady = source.indexOf("status: 'ready'", precomputeCall);
|
|
12
|
+
const cubeRegistration = source.indexOf('const cubeHandle = regResult.value;', precomputeCall);
|
|
13
|
+
const mapPublication = source.indexOf(
|
|
14
|
+
'this.cubemapIdempotentMap.set(sourceId, cubeHandle);',
|
|
15
|
+
precomputeCall,
|
|
16
|
+
);
|
|
17
|
+
expect(precomputeCall).toBeGreaterThan(-1);
|
|
18
|
+
expect(precomputeSuccess).toBeGreaterThan(precomputeCall);
|
|
19
|
+
expect(sourceReady).toBeGreaterThan(precomputeSuccess);
|
|
20
|
+
expect(cubeRegistration).toBeGreaterThan(precomputeSuccess);
|
|
21
|
+
expect(mapPublication).toBeGreaterThan(precomputeSuccess);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('keeps pending publication before asynchronous pipeline work', () => {
|
|
25
|
+
const pending = source.indexOf("status: 'pending'");
|
|
26
|
+
const precompute = source.indexOf('const runResult = await runIblPrecompute({');
|
|
27
|
+
expect(pending).toBeGreaterThan(-1);
|
|
28
|
+
expect(pending).toBeLessThan(precompute);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('promotes the IBL candidate only after the completion fence', () => {
|
|
32
|
+
const runStart = iblSource.indexOf('export async function runIblPrecompute(');
|
|
33
|
+
const fence = iblSource.indexOf('await device.queue.onSubmittedWorkDone()', runStart);
|
|
34
|
+
const promotion = iblSource.indexOf(
|
|
35
|
+
'cache.irradianceTexture = promoted.irradianceTexture',
|
|
36
|
+
runStart,
|
|
37
|
+
);
|
|
38
|
+
const ready = source.indexOf("status: 'ready'", source.indexOf('_uploadCubemapFromEquirect'));
|
|
39
|
+
expect(runStart).toBeGreaterThan(-1);
|
|
40
|
+
expect(fence).toBeGreaterThan(runStart);
|
|
41
|
+
expect(promotion).toBeGreaterThan(fence);
|
|
42
|
+
expect(ready).toBeGreaterThan(fence);
|
|
43
|
+
expect(iblSource.slice(runStart, fence)).not.toContain('cache.irradianceTexture =');
|
|
44
|
+
expect(iblSource.slice(runStart, fence)).not.toContain('cache.prefilterTexture =');
|
|
45
|
+
expect(iblSource.slice(runStart, fence)).not.toContain('cache.brdfLutTexture =');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('awaits precompute before residency publication', () => {
|
|
49
|
+
const call = source.indexOf('const runResult = await runIblPrecompute({');
|
|
50
|
+
const success = source.indexOf('if (!runResult.ok)', call);
|
|
51
|
+
const ready = source.indexOf("status: 'ready'", call);
|
|
52
|
+
const map = source.indexOf('this.cubemapIdempotentMap.set(sourceId, cubeHandle);', call);
|
|
53
|
+
expect(call).toBeGreaterThan(-1);
|
|
54
|
+
expect(success).toBeGreaterThan(call);
|
|
55
|
+
expect(ready).toBeGreaterThan(success);
|
|
56
|
+
expect(map).toBeGreaterThan(success);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('keeps candidate and transient resources under the DeviceScope owner', () => {
|
|
60
|
+
expect(iblSource).toContain("scope._adopt('texture', texture");
|
|
61
|
+
expect(source).toContain("scope._adopt('texture', equirectGpuTex");
|
|
62
|
+
expect(source).toContain("scope._adopt('buffer', faceBuf");
|
|
63
|
+
expect(source).toContain("scope._adopt('buffer', prefBuf");
|
|
64
|
+
expect(source).toContain("scope._adopt('buffer', cubeVertex");
|
|
65
|
+
const fence = iblSource.indexOf('await device.queue.onSubmittedWorkDone()');
|
|
66
|
+
const candidateWrites = iblSource.indexOf('candidate.irradianceTexture = adoptIblTexture');
|
|
67
|
+
expect(candidateWrites).toBeLessThan(fence);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('fences each IBL stage before recording the next stage', () => {
|
|
71
|
+
const runStart = iblSource.indexOf('export async function runIblPrecompute(');
|
|
72
|
+
const stages = ['equirect-to-cube', 'prefilter', 'brdf-lut'];
|
|
73
|
+
let previous = runStart;
|
|
74
|
+
for (const stage of stages) {
|
|
75
|
+
const marker = iblSource.indexOf(`await submitStage('${stage}'`, previous);
|
|
76
|
+
expect(marker).toBeGreaterThan(previous);
|
|
77
|
+
previous = marker;
|
|
78
|
+
}
|
|
79
|
+
expect(iblSource.match(/await submitStage\('/g)).toHaveLength(stages.length);
|
|
80
|
+
expect(iblSource).toContain('await device.queue.onSubmittedWorkDone()');
|
|
81
|
+
expect(
|
|
82
|
+
iblSource.indexOf('cache.irradianceTexture = promoted.irradianceTexture'),
|
|
83
|
+
).toBeGreaterThan(previous);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not promote or increment counters after a failed stage', () => {
|
|
87
|
+
const runStart = iblSource.indexOf('export async function runIblPrecompute(');
|
|
88
|
+
const promotion = iblSource.indexOf(
|
|
89
|
+
'cache.irradianceTexture = promoted.irradianceTexture',
|
|
90
|
+
runStart,
|
|
91
|
+
);
|
|
92
|
+
const counters = iblSource.indexOf('cache.irradianceBakeCount += 1', runStart);
|
|
93
|
+
const stageFailure = iblSource.indexOf('return err(badAlloc', runStart);
|
|
94
|
+
expect(stageFailure).toBeGreaterThan(runStart);
|
|
95
|
+
expect(stageFailure).toBeLessThan(promotion);
|
|
96
|
+
expect(promotion).toBeLessThan(counters);
|
|
97
|
+
expect(iblSource).toContain('stage}-device-scope-generation');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('fences each irradiance face before the next face', () => {
|
|
101
|
+
const runStart = iblSource.indexOf('export async function runIblPrecompute(');
|
|
102
|
+
const irradianceStart = iblSource.indexOf('// (b) irradiance convolve', runStart);
|
|
103
|
+
const prefilterStart = iblSource.indexOf('// (c) prefilter env', irradianceStart);
|
|
104
|
+
const faceMarkers = iblSource.match(/submitStage\(`irradiance-face-\$\{face\}`/g) ?? [];
|
|
105
|
+
expect(faceMarkers).toHaveLength(1);
|
|
106
|
+
expect(iblSource.slice(irradianceStart, prefilterStart)).toMatch(
|
|
107
|
+
/await submitStage\(`irradiance-face-\$\{face\}`, true\)/,
|
|
108
|
+
);
|
|
109
|
+
expect(iblSource.slice(irradianceStart, prefilterStart)).not.toContain(
|
|
110
|
+
"await submitStage('irradiance', true)",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { InstanceProjectionStore } from '../instances';
|
|
4
|
+
|
|
5
|
+
function matrices(x: number): Float32Array {
|
|
6
|
+
return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, 0, 0, 1]);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('renderer instance projection', () => {
|
|
10
|
+
it('derives independent complete snapshots for consumers that skip different updates', () => {
|
|
11
|
+
const world = new World();
|
|
12
|
+
const first = new InstanceProjectionStore();
|
|
13
|
+
const late = new InstanceProjectionStore();
|
|
14
|
+
const before = first.project(world, 1, matrices(0));
|
|
15
|
+
late.project(world, 1, matrices(0));
|
|
16
|
+
first.project(world, 1, matrices(1));
|
|
17
|
+
first.project(world, 1, matrices(2));
|
|
18
|
+
const latest = late.project(world, 1, matrices(2));
|
|
19
|
+
expect(before.transforms[12]).toBe(0);
|
|
20
|
+
expect(latest.transforms[12]).toBe(2);
|
|
21
|
+
const stable = late.project(world, 1, matrices(2));
|
|
22
|
+
expect(stable.transforms).toBe(latest.transforms);
|
|
23
|
+
expect(stable.revision).toBe(latest.revision);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('does not alias identical entity numbers across World identities', () => {
|
|
27
|
+
const projection = new InstanceProjectionStore();
|
|
28
|
+
const a = projection.project(new World(), 1, matrices(10));
|
|
29
|
+
const b = projection.project(new World(), 1, matrices(99));
|
|
30
|
+
expect(a.collectionId).not.toBe(b.collectionId);
|
|
31
|
+
expect(a.transforms[12]).toBe(10);
|
|
32
|
+
expect(b.transforms[12]).toBe(99);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('rebuilds from unchanged source after dropping renderer resources', () => {
|
|
36
|
+
const world = new World();
|
|
37
|
+
const transforms = matrices(4);
|
|
38
|
+
const projection = new InstanceProjectionStore();
|
|
39
|
+
projection.project(world, 1, transforms);
|
|
40
|
+
projection.dispose();
|
|
41
|
+
expect(projection.project(world, 1, transforms).transforms).toEqual(transforms);
|
|
42
|
+
expect(new InstanceProjectionStore().project(world, 1, transforms).transforms).toEqual(
|
|
43
|
+
transforms,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { HANDLE_CUBE } from '@forgeax/engine-assets-runtime';
|
|
2
|
+
import { World } from '@forgeax/engine-ecs';
|
|
3
|
+
import { registerPropagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { Camera } from '../components/camera';
|
|
6
|
+
import { Instances } from '../components/instances';
|
|
7
|
+
import { MeshFilter } from '../components/mesh-filter';
|
|
8
|
+
import { MeshRenderer } from '../components/mesh-renderer';
|
|
9
|
+
import { extractFrames } from '../render-system-extract';
|
|
10
|
+
|
|
11
|
+
function matrices(count: number, x: number): Float32Array {
|
|
12
|
+
const out = new Float32Array(count * 16);
|
|
13
|
+
for (let i = 0; i < count; i++) {
|
|
14
|
+
out[i * 16] = out[i * 16 + 5] = out[i * 16 + 10] = out[i * 16 + 15] = 1;
|
|
15
|
+
out[i * 16 + 12] = x;
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function scene(count: number, x: number) {
|
|
21
|
+
const world = new World();
|
|
22
|
+
registerPropagateTransforms(world);
|
|
23
|
+
world
|
|
24
|
+
.spawn(
|
|
25
|
+
{ component: Transform, data: { pos: [0, 0, 5] } },
|
|
26
|
+
{ component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 100 } },
|
|
27
|
+
)
|
|
28
|
+
.unwrap();
|
|
29
|
+
const entity = world
|
|
30
|
+
.spawn(
|
|
31
|
+
{ component: Transform, data: {} },
|
|
32
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
33
|
+
{ component: MeshRenderer, data: {} },
|
|
34
|
+
{ component: Instances, data: { transforms: matrices(count, x) } },
|
|
35
|
+
)
|
|
36
|
+
.unwrap();
|
|
37
|
+
world.update(0).unwrap();
|
|
38
|
+
return { world, entity };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('World-owned instance authoring', () => {
|
|
42
|
+
it.each([1500, 10000, 20000])('extracts %s matrices without creating a Renderer', (count) => {
|
|
43
|
+
const { world } = scene(count, 0);
|
|
44
|
+
const snapshot = extractFrames([world], 0).renderables[0]?.instances;
|
|
45
|
+
expect(snapshot?.instanceCount).toBe(count);
|
|
46
|
+
expect(snapshot?.transforms).toEqual(matrices(count, 0));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('keeps worlds isolated and lets independent consumers observe skipped updates', () => {
|
|
50
|
+
const a = scene(2, 0);
|
|
51
|
+
const b = scene(2, 1);
|
|
52
|
+
const before = extractFrames([a.world], 0).renderables[0]?.instances;
|
|
53
|
+
expect(extractFrames([b.world], 0).renderables[0]?.instances?.transforms[12]).toBe(1);
|
|
54
|
+
a.world.set(a.entity, Instances, { transforms: matrices(2, 0.5) }).unwrap();
|
|
55
|
+
extractFrames([a.world], 0);
|
|
56
|
+
a.world.set(a.entity, Instances, { transforms: matrices(2, -0.5) }).unwrap();
|
|
57
|
+
const late = extractFrames([a.world], 0).renderables[0]?.instances;
|
|
58
|
+
expect(late?.transforms[12]).toBe(-0.5);
|
|
59
|
+
expect(before?.transforms[12]).toBe(0);
|
|
60
|
+
expect(extractFrames([b.world], 0).renderables[0]?.instances?.transforms[12]).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
});
|