@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,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
classifyRecoveryCapability,
|
|
4
|
+
type RecoveryCapabilityFacts,
|
|
5
|
+
} from '../gpu-driven/production-raster';
|
|
6
|
+
|
|
7
|
+
const allCapabilities: RecoveryCapabilityFacts = {
|
|
8
|
+
generation: 7,
|
|
9
|
+
compute: true,
|
|
10
|
+
storageBuffer: true,
|
|
11
|
+
indirectDrawing: true,
|
|
12
|
+
multisample: true,
|
|
13
|
+
shaderCompilation: true,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('renderer recovery capability matrix', () => {
|
|
17
|
+
it('publishes the standard fallback when optional capability is absent', () => {
|
|
18
|
+
const result = classifyRecoveryCapability({
|
|
19
|
+
...allCapabilities,
|
|
20
|
+
multisample: false,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
status: 'fallback',
|
|
25
|
+
generation: 7,
|
|
26
|
+
disabled: [],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('reports optional GPU-driven work as disabled without allocation', () => {
|
|
31
|
+
const result = classifyRecoveryCapability({
|
|
32
|
+
...allCapabilities,
|
|
33
|
+
compute: false,
|
|
34
|
+
storageBuffer: false,
|
|
35
|
+
indirectDrawing: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result).toEqual({
|
|
39
|
+
status: 'disabled',
|
|
40
|
+
generation: 7,
|
|
41
|
+
disabled: ['gpu-driven'],
|
|
42
|
+
allocations: 0,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('refuses a generation that lacks a core shader capability', () => {
|
|
47
|
+
const result = classifyRecoveryCapability({
|
|
48
|
+
...allCapabilities,
|
|
49
|
+
shaderCompilation: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
status: 'refused',
|
|
54
|
+
generation: 7,
|
|
55
|
+
failedOwner: 'shader-material-pipeline',
|
|
56
|
+
resourceKind: 'pipeline',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createRecoveryFailedError,
|
|
4
|
+
RECOVERY_PHASES,
|
|
5
|
+
type RecoveryFailureDetail,
|
|
6
|
+
} from '../errors/recover';
|
|
7
|
+
|
|
8
|
+
const receipt = Object.freeze({ owner: 'renderer', generation: 4, resourceCount: 3 });
|
|
9
|
+
|
|
10
|
+
function detail(overrides: Partial<RecoveryFailureDetail> = {}): RecoveryFailureDetail {
|
|
11
|
+
return {
|
|
12
|
+
phase: 'compile-graph',
|
|
13
|
+
oldGeneration: 4,
|
|
14
|
+
candidateGeneration: 5,
|
|
15
|
+
attempt: 2,
|
|
16
|
+
elapsedMs: 120,
|
|
17
|
+
retryable: true,
|
|
18
|
+
guidance: 'retry',
|
|
19
|
+
owner: 'standard-pipeline',
|
|
20
|
+
resourceKind: 'pipeline',
|
|
21
|
+
lastOutcome: 'failed',
|
|
22
|
+
rehydratedRoots: 3,
|
|
23
|
+
staleLossEvents: 1,
|
|
24
|
+
cause: new Error('candidate failed'),
|
|
25
|
+
cleanupFailures: [],
|
|
26
|
+
receipt,
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('renderer recovery error contract', () => {
|
|
32
|
+
it('keeps recovery phases and guidance closed and machine-readable', () => {
|
|
33
|
+
expect(RECOVERY_PHASES).toEqual([
|
|
34
|
+
'quiesce',
|
|
35
|
+
'acquire-adapter',
|
|
36
|
+
'acquire-device',
|
|
37
|
+
'rehydrate',
|
|
38
|
+
'compile-graph',
|
|
39
|
+
'publish',
|
|
40
|
+
'cleanup',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const failure = createRecoveryFailedError(detail());
|
|
44
|
+
expect(failure.code).toBe('recovery-failed');
|
|
45
|
+
expect(failure.expected).toContain('complete replacement');
|
|
46
|
+
expect(failure.hint).toContain('detail');
|
|
47
|
+
expect(failure.detail.phase).toBe('compile-graph');
|
|
48
|
+
expect(failure.detail.oldGeneration).toBe(4);
|
|
49
|
+
expect(failure.detail.candidateGeneration).toBe(5);
|
|
50
|
+
expect(failure.detail.attempt).toBe(2);
|
|
51
|
+
expect(failure.detail.elapsedMs).toBe(120);
|
|
52
|
+
expect(failure.detail.retryable).toBe(true);
|
|
53
|
+
expect(failure.detail.guidance).toBe('retry');
|
|
54
|
+
expect(failure.detail.owner).toBe('standard-pipeline');
|
|
55
|
+
expect(failure.detail.resourceKind).toBe('pipeline');
|
|
56
|
+
expect(failure.detail.lastOutcome).toBe('failed');
|
|
57
|
+
expect(failure.detail.rehydratedRoots).toBe(3);
|
|
58
|
+
expect(failure.detail.staleLossEvents).toBe(1);
|
|
59
|
+
expect(failure.detail.cleanupFailures).toEqual([]);
|
|
60
|
+
expect(Object.isFrozen(failure.detail)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it.each([
|
|
64
|
+
['retry', true],
|
|
65
|
+
['repair-owner', true],
|
|
66
|
+
['rebuild-renderer', false],
|
|
67
|
+
] as const)('preserves the %s guidance branch', (guidance, retryable) => {
|
|
68
|
+
const failure = createRecoveryFailedError(detail({ guidance, retryable }));
|
|
69
|
+
expect(failure.detail.guidance).toBe(guidance);
|
|
70
|
+
expect(failure.detail.retryable).toBe(retryable);
|
|
71
|
+
expect(failure.message).not.toContain('retry');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { RhiNullCommandEncoder, RhiNullDevice, RhiNullQueue, rhi } from '@forgeax/engine-rhi-null';
|
|
2
|
+
import { ok } from '@forgeax/engine-types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
acquireDeviceGeneration,
|
|
6
|
+
probeCandidateGraphExecution,
|
|
7
|
+
runRecoveryStep,
|
|
8
|
+
} from '../assembly/recovery/recovery-attempt';
|
|
9
|
+
import {
|
|
10
|
+
createRecoveryContinuation,
|
|
11
|
+
createRecoveryDeadline,
|
|
12
|
+
createRendererLifecycle,
|
|
13
|
+
createSingleFlight,
|
|
14
|
+
RECOVERY_ADAPTER_DEADLINE_MS,
|
|
15
|
+
RECOVERY_DEVICE_DEADLINE_MS,
|
|
16
|
+
RECOVERY_PHASES,
|
|
17
|
+
RECOVERY_TOTAL_DEADLINE_MS,
|
|
18
|
+
} from '../assembly/renderer-lifecycle';
|
|
19
|
+
|
|
20
|
+
describe('renderer recovery lifecycle', () => {
|
|
21
|
+
it('exhausts the public lifecycle states and rejects illegal transitions', () => {
|
|
22
|
+
const lifecycle = createRendererLifecycle();
|
|
23
|
+
|
|
24
|
+
expect(lifecycle.state()).toBe('alive');
|
|
25
|
+
expect(lifecycle.transition('device-lost')).toBe(true);
|
|
26
|
+
expect(lifecycle.transition('recovering')).toBe(true);
|
|
27
|
+
expect(lifecycle.transition('alive')).toBe(true);
|
|
28
|
+
expect(lifecycle.transition('faulted')).toBe(false);
|
|
29
|
+
expect(lifecycle.transition('disposed')).toBe(true);
|
|
30
|
+
expect(lifecycle.transition('alive')).toBe(false);
|
|
31
|
+
expect(lifecycle.states()).toEqual([
|
|
32
|
+
'alive',
|
|
33
|
+
'device-lost',
|
|
34
|
+
'recovering',
|
|
35
|
+
'faulted',
|
|
36
|
+
'disposed',
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('shares one recovery promise and one candidate for concurrent callers', async () => {
|
|
41
|
+
let attempts = 0;
|
|
42
|
+
let release: (() => void) | undefined;
|
|
43
|
+
const flight = createSingleFlight(async () => {
|
|
44
|
+
attempts += 1;
|
|
45
|
+
await new Promise<void>((resolve) => {
|
|
46
|
+
release = resolve;
|
|
47
|
+
});
|
|
48
|
+
return { candidate: attempts };
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const first = flight.run();
|
|
52
|
+
const second = flight.run();
|
|
53
|
+
expect(first).toBe(second);
|
|
54
|
+
expect(flight.inFlight()).toBe(first);
|
|
55
|
+
expect(attempts).toBe(1);
|
|
56
|
+
|
|
57
|
+
release?.();
|
|
58
|
+
await expect(first).resolves.toEqual({ candidate: 1 });
|
|
59
|
+
expect(flight.inFlight()).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('enforces the 20 second attempt and 10 second acquisition budgets', () => {
|
|
63
|
+
const deadline = createRecoveryDeadline(1000);
|
|
64
|
+
|
|
65
|
+
expect(deadline.totalDeadlineMs).toBe(RECOVERY_TOTAL_DEADLINE_MS);
|
|
66
|
+
expect(deadline.adapterDeadlineMs).toBe(RECOVERY_ADAPTER_DEADLINE_MS);
|
|
67
|
+
expect(deadline.deviceDeadlineMs).toBe(RECOVERY_DEVICE_DEADLINE_MS);
|
|
68
|
+
expect(deadline.isValid('acquire-adapter', 10_999)).toBe(true);
|
|
69
|
+
expect(deadline.isValid('acquire-adapter', 11_000)).toBe(false);
|
|
70
|
+
expect(deadline.isValid('acquire-device', 11_000)).toBe(false);
|
|
71
|
+
expect(deadline.isValid('compile-graph', 20_999)).toBe(true);
|
|
72
|
+
expect(deadline.isValid('compile-graph', 21_000)).toBe(false);
|
|
73
|
+
expect(RECOVERY_PHASES).toEqual([
|
|
74
|
+
'quiesce',
|
|
75
|
+
'acquire-adapter',
|
|
76
|
+
'acquire-device',
|
|
77
|
+
'rehydrate',
|
|
78
|
+
'compile-graph',
|
|
79
|
+
'publish',
|
|
80
|
+
'cleanup',
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('reports acquisition phases in their execution order', async () => {
|
|
85
|
+
const phases: string[] = [];
|
|
86
|
+
const result = await acquireDeviceGeneration(
|
|
87
|
+
{ rhi },
|
|
88
|
+
{} as HTMLCanvasElement,
|
|
89
|
+
undefined,
|
|
90
|
+
undefined,
|
|
91
|
+
(phase) => phases.push(phase),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result.ok).toBe(true);
|
|
95
|
+
expect(phases).toEqual(['acquire-adapter', 'acquire-device']);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('treats the null graph probe as structural-only and rejects a non-null pixel mismatch', async () => {
|
|
99
|
+
const adapter = (await rhi.requestAdapter()).unwrap();
|
|
100
|
+
const nullDevice = (await adapter.requestDevice()).unwrap();
|
|
101
|
+
const texture = nullDevice
|
|
102
|
+
.createTexture({
|
|
103
|
+
label: 'recovery-probe-test.texture',
|
|
104
|
+
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
105
|
+
mipLevelCount: 1,
|
|
106
|
+
sampleCount: 1,
|
|
107
|
+
dimension: '2d',
|
|
108
|
+
format: 'rgba8unorm',
|
|
109
|
+
usage: 0x11,
|
|
110
|
+
viewFormats: undefined,
|
|
111
|
+
textureBindingViewDimension: undefined,
|
|
112
|
+
})
|
|
113
|
+
.unwrap();
|
|
114
|
+
const view = nullDevice.createTextureView(texture, { dimension: '2d' }).unwrap();
|
|
115
|
+
const nullProbe = await probeCandidateGraphExecution(nullDevice, {
|
|
116
|
+
texture,
|
|
117
|
+
view,
|
|
118
|
+
format: 'rgba8unorm',
|
|
119
|
+
});
|
|
120
|
+
expect(nullProbe.ok).toBe(true);
|
|
121
|
+
|
|
122
|
+
class PixellessNonNullDevice extends RhiNullDevice {
|
|
123
|
+
override get caps() {
|
|
124
|
+
return { ...super.caps, backendKind: 'webgpu' as const };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const nonNullDevice = new PixellessNonNullDevice(
|
|
128
|
+
new RhiNullQueue(),
|
|
129
|
+
(bookkeeper, device) => new RhiNullCommandEncoder(bookkeeper, device),
|
|
130
|
+
);
|
|
131
|
+
const nonNullTexture = nonNullDevice
|
|
132
|
+
.createTexture({
|
|
133
|
+
label: 'recovery-probe-test.non-null-texture',
|
|
134
|
+
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
135
|
+
mipLevelCount: 1,
|
|
136
|
+
sampleCount: 1,
|
|
137
|
+
dimension: '2d',
|
|
138
|
+
format: 'rgba8unorm',
|
|
139
|
+
usage: 0x11,
|
|
140
|
+
viewFormats: undefined,
|
|
141
|
+
textureBindingViewDimension: undefined,
|
|
142
|
+
})
|
|
143
|
+
.unwrap();
|
|
144
|
+
const nonNullView = nonNullDevice
|
|
145
|
+
.createTextureView(nonNullTexture, { dimension: '2d' })
|
|
146
|
+
.unwrap();
|
|
147
|
+
const nonNullProbe = await probeCandidateGraphExecution(nonNullDevice, {
|
|
148
|
+
texture: nonNullTexture,
|
|
149
|
+
view: nonNullView,
|
|
150
|
+
format: 'rgba8unorm',
|
|
151
|
+
});
|
|
152
|
+
expect(nonNullProbe.ok).toBe(false);
|
|
153
|
+
if (!nonNullProbe.ok) expect(nonNullProbe.error.code).toBe('webgpu-runtime-error');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('invalidates late continuation and cleans the candidate exactly once', () => {
|
|
157
|
+
let cleanupCount = 0;
|
|
158
|
+
const continuation = createRecoveryContinuation(createRecoveryDeadline(0), () => {
|
|
159
|
+
cleanupCount += 1;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(continuation.isValid('compile-graph', 19_999)).toBe(true);
|
|
163
|
+
continuation.abandon(20_000);
|
|
164
|
+
expect(continuation.isValid('compile-graph', 20_000)).toBe(false);
|
|
165
|
+
continuation.cleanupOnce();
|
|
166
|
+
continuation.cleanupOnce();
|
|
167
|
+
expect(cleanupCount).toBe(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('invalidates an async step on timeout before a late completion can write', async () => {
|
|
171
|
+
let cleanupCount = 0;
|
|
172
|
+
let lateWrites = 0;
|
|
173
|
+
const startedAt = Date.now();
|
|
174
|
+
const deadline = {
|
|
175
|
+
startedAt,
|
|
176
|
+
totalDeadlineMs: 1,
|
|
177
|
+
adapterDeadlineMs: 1,
|
|
178
|
+
deviceDeadlineMs: 1,
|
|
179
|
+
deadlineAt: startedAt + 1,
|
|
180
|
+
beginDeviceAcquisition: (_now: number) => undefined,
|
|
181
|
+
isValid: (
|
|
182
|
+
_phase:
|
|
183
|
+
| 'quiesce'
|
|
184
|
+
| 'acquire-adapter'
|
|
185
|
+
| 'acquire-device'
|
|
186
|
+
| 'rehydrate'
|
|
187
|
+
| 'compile-graph'
|
|
188
|
+
| 'publish'
|
|
189
|
+
| 'cleanup',
|
|
190
|
+
now: number,
|
|
191
|
+
) => now < startedAt + 1,
|
|
192
|
+
elapsed: (now: number) => Math.max(0, now - startedAt),
|
|
193
|
+
};
|
|
194
|
+
const continuation = createRecoveryContinuation(deadline, () => {
|
|
195
|
+
cleanupCount += 1;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const outcome = await runRecoveryStep(
|
|
199
|
+
async (attemptToken) => {
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
201
|
+
if (attemptToken?.isValid('compile-graph', Date.now())) lateWrites += 1;
|
|
202
|
+
return ok(undefined);
|
|
203
|
+
},
|
|
204
|
+
'compile-graph',
|
|
205
|
+
deadline,
|
|
206
|
+
Date.now,
|
|
207
|
+
continuation,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(outcome.kind).toBe('timeout');
|
|
211
|
+
if (outcome.kind === 'timeout') expect(outcome.timeout.cause.code).toBe('webgpu-runtime-error');
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
213
|
+
expect(lateWrites).toBe(0);
|
|
214
|
+
expect(cleanupCount).toBe(1);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { TaaHistoryStore } from '../temporal/taa-history-store';
|
|
3
|
+
|
|
4
|
+
type RecoveryReceipt = {
|
|
5
|
+
readonly deviceGeneration: number;
|
|
6
|
+
readonly completed: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type RecoverableHistory = TaaHistoryStore & {
|
|
10
|
+
recoverForGeneration(deviceGeneration: number): void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function recover(store: TaaHistoryStore, deviceGeneration: number): void {
|
|
14
|
+
(store as RecoverableHistory).recoverForGeneration(deviceGeneration);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function commit(store: TaaHistoryStore, receipt: RecoveryReceipt): void {
|
|
18
|
+
const attempt = store.begin('device-generation');
|
|
19
|
+
attempt.commit(receipt as never);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('M4 / m4_t2 — target history recovery keeps logical token', () => {
|
|
23
|
+
it('publishes uninitialized replacement storage with neutral fallback', () => {
|
|
24
|
+
const store = new TaaHistoryStore();
|
|
25
|
+
const token = (
|
|
26
|
+
store.inspect() as typeof store.inspect extends () => infer T ? T : never & { token: string }
|
|
27
|
+
).token;
|
|
28
|
+
|
|
29
|
+
recover(store, 8);
|
|
30
|
+
|
|
31
|
+
expect(store.inspect()).toMatchObject({
|
|
32
|
+
token,
|
|
33
|
+
deviceGeneration: 8,
|
|
34
|
+
content: 'uninitialized',
|
|
35
|
+
fallback: 'neutral-texture',
|
|
36
|
+
valid: false,
|
|
37
|
+
requestVersion: 0,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('raises history only for a matching completed receipt', () => {
|
|
42
|
+
const store = new TaaHistoryStore();
|
|
43
|
+
recover(store, 8);
|
|
44
|
+
const before = store.inspect();
|
|
45
|
+
|
|
46
|
+
commit(store, { deviceGeneration: 7, completed: true });
|
|
47
|
+
expect(store.inspect()).toMatchObject({
|
|
48
|
+
token: before.token,
|
|
49
|
+
deviceGeneration: 8,
|
|
50
|
+
content: 'uninitialized',
|
|
51
|
+
valid: false,
|
|
52
|
+
requestVersion: 0,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
commit(store, { deviceGeneration: 8, completed: false });
|
|
56
|
+
expect(store.inspect().valid).toBe(false);
|
|
57
|
+
|
|
58
|
+
commit(store, { deviceGeneration: 8, completed: true });
|
|
59
|
+
expect(store.inspect()).toMatchObject({
|
|
60
|
+
token: before.token,
|
|
61
|
+
deviceGeneration: 8,
|
|
62
|
+
content: 'active',
|
|
63
|
+
valid: true,
|
|
64
|
+
requestVersion: 1,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not invent once or on-demand requests during replacement', () => {
|
|
69
|
+
const store = new TaaHistoryStore();
|
|
70
|
+
recover(store, 9);
|
|
71
|
+
const inspection = store.inspect() as typeof store.inspect extends () => infer T
|
|
72
|
+
? T
|
|
73
|
+
: never & {
|
|
74
|
+
requestVersion: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
expect(inspection.requestVersion).toBe(0);
|
|
78
|
+
expect(inspection.content).toBe('uninitialized');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -58,22 +58,19 @@ const standardPbrVariants = (() => {
|
|
|
58
58
|
const variants = [];
|
|
59
59
|
for (const cluster of [false, true]) {
|
|
60
60
|
for (const storage of [false, true]) {
|
|
61
|
-
for (const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
: sortedDefines.map(([key, value]) => `${key}=${value}`).join('+');
|
|
75
|
-
variants.push({ definesKey, defines, composedWgsl: pbrWgsl });
|
|
76
|
-
}
|
|
61
|
+
for (const vertexColor of [false, true]) {
|
|
62
|
+
const defines = {
|
|
63
|
+
CLUSTER_FORWARD_AVAILABLE: cluster,
|
|
64
|
+
STORAGE_BUFFER_AVAILABLE: storage,
|
|
65
|
+
VERTEX_COLOR_AVAILABLE: vertexColor,
|
|
66
|
+
};
|
|
67
|
+
const sortedDefines = Object.entries(defines).sort(([left], [right]) =>
|
|
68
|
+
left < right ? -1 : left > right ? 1 : 0,
|
|
69
|
+
);
|
|
70
|
+
const definesKey = sortedDefines.every(([, value]) => value)
|
|
71
|
+
? ''
|
|
72
|
+
: sortedDefines.map(([key, value]) => `${key}=${value}`).join('+');
|
|
73
|
+
variants.push({ definesKey, defines, composedWgsl: pbrWgsl });
|
|
77
74
|
}
|
|
78
75
|
}
|
|
79
76
|
}
|
|
@@ -20,7 +20,7 @@ const fragmentConsumerSources = [
|
|
|
20
20
|
pbrPipelineSource,
|
|
21
21
|
iblPipelineCacheSource,
|
|
22
22
|
readFileSync(new URL('../ibl/skylight-bind-group.ts', import.meta.url), 'utf8'),
|
|
23
|
-
readFileSync(new URL('../assembly/
|
|
23
|
+
readFileSync(new URL('../assembly/webgpu-renderer.ts', import.meta.url), 'utf8'),
|
|
24
24
|
];
|
|
25
25
|
|
|
26
26
|
describe('shader stage owner', () => {
|
|
@@ -45,6 +45,39 @@ describe('custom skinned mesh motion consumer contract', () => {
|
|
|
45
45
|
expect(writes[1]?.data[12]).toBe(0.5);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
it('keeps stable uniform palette slices isolated across frame-pool resets', () => {
|
|
49
|
+
const device = {
|
|
50
|
+
limits: { maxUniformBufferBindingSize: 65536 },
|
|
51
|
+
createBuffer: () => ok({} as Buffer),
|
|
52
|
+
destroyBuffer: () => undefined,
|
|
53
|
+
queue: { writeBuffer: () => ok(undefined) },
|
|
54
|
+
} as unknown as RhiDevice;
|
|
55
|
+
const allocator = createSkinPaletteAllocator(device, 65536, false);
|
|
56
|
+
const first = allocator.allocateSliceFor('world:1', 1);
|
|
57
|
+
allocator.resetForFrame();
|
|
58
|
+
const second = allocator.allocateSliceFor('world:2', 1);
|
|
59
|
+
|
|
60
|
+
expect(second.buffer).not.toBe(first.buffer);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('does not reuse a split storage range beyond its binding window', () => {
|
|
64
|
+
let bufferId = 0;
|
|
65
|
+
const device = {
|
|
66
|
+
limits: { maxStorageBufferBindingSize: 65536 },
|
|
67
|
+
createBuffer: () => ok({ id: bufferId++ } as unknown as Buffer),
|
|
68
|
+
queue: { writeBuffer: () => ok(undefined) },
|
|
69
|
+
} as unknown as RhiDevice;
|
|
70
|
+
const allocator = createSkinPaletteAllocator(device, 65536, true);
|
|
71
|
+
|
|
72
|
+
const full = allocator.allocateSliceFor('world:full', 255);
|
|
73
|
+
allocator.releaseSliceFor('world:full');
|
|
74
|
+
const firstSmall = allocator.allocateSliceFor('world:first', 1);
|
|
75
|
+
const secondSmall = allocator.allocateSliceFor('world:second', 1);
|
|
76
|
+
|
|
77
|
+
expect(firstSmall.buffer).toBe(full.buffer);
|
|
78
|
+
expect(secondSmall.buffer).not.toBe(full.buffer);
|
|
79
|
+
});
|
|
80
|
+
|
|
48
81
|
it('prepares both URP and HDRP pbr-skin consumers with the skin-cluster contract', () => {
|
|
49
82
|
const variants = selectSkinPrewarmVariants(
|
|
50
83
|
{
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
SHADOW_CASTER_SHADER_ID,
|
|
10
10
|
shadowCasterVariantSet,
|
|
11
11
|
} from '../pbr-pipeline.js';
|
|
12
|
+
import { shadowShaderMap } from '../record/shadow-pass.js';
|
|
12
13
|
|
|
13
14
|
describe('skinned shadow caster', () => {
|
|
14
15
|
it('derives closed capability variants without changing the shader identity', () => {
|
|
@@ -52,8 +53,7 @@ describe('skinned shadow caster', () => {
|
|
|
52
53
|
expect(shader).toContain('let skinMatrix = palette[');
|
|
53
54
|
expect(record).toContain('entry.source.skin.byteOffset');
|
|
54
55
|
expect(record).toContain("'shadow-pbr-skin-mesh'");
|
|
55
|
-
expect(builder).toContain(
|
|
56
|
-
expect(builder).toContain('? [...ctx.vertexBuffers]');
|
|
56
|
+
expect(builder).toContain('buffers: [...ctx.vertexBuffers]');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it('keeps mixed static and skinned shadow draws on matching variants and bindings', () => {
|
|
@@ -129,4 +129,32 @@ describe('skinned shadow caster', () => {
|
|
|
129
129
|
);
|
|
130
130
|
expect(recordSource).toContain('binding: 12');
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
it('keeps the dispatch material handle, snapshot, and frame slot together', () => {
|
|
134
|
+
const paramSnapshot = { alphaClipThreshold: 0.4 };
|
|
135
|
+
const context = {
|
|
136
|
+
dispatch: [
|
|
137
|
+
{
|
|
138
|
+
renderableIndex: 4,
|
|
139
|
+
materialHandle: 42,
|
|
140
|
+
materialShaderId: 'forgeax::default-standard-pbr',
|
|
141
|
+
paramSnapshot,
|
|
142
|
+
tags: { LightMode: 'ShadowCaster' },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
validatedOrdered: [{ renderableIndex: 4 }],
|
|
146
|
+
materialSlotIndices: [[3, 7]],
|
|
147
|
+
materialSlots: Array.from({ length: 8 }, (_, slot) => ({
|
|
148
|
+
materialHandle: slot === 7 ? 42 : 11,
|
|
149
|
+
})),
|
|
150
|
+
} as never;
|
|
151
|
+
|
|
152
|
+
const bindings = shadowShaderMap(context);
|
|
153
|
+
expect(bindings.get(4)?.get(42)).toMatchObject({
|
|
154
|
+
materialHandle: 42,
|
|
155
|
+
materialShaderId: SHADOW_CASTER_SHADER_ID,
|
|
156
|
+
materialSlot: 7,
|
|
157
|
+
paramSnapshot,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
132
160
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Materials } from '../materials.js';
|
|
3
|
+
|
|
4
|
+
describe('Standard pass projection', () => {
|
|
5
|
+
it('keeps default and custom base surfaces on the base pass family', () => {
|
|
6
|
+
const material = Materials.standard({ baseColor: [1, 1, 1, 1] });
|
|
7
|
+
expect(material.passes?.map((pass) => pass.name)).toEqual([
|
|
8
|
+
'forward',
|
|
9
|
+
'deferred',
|
|
10
|
+
'shadow-caster',
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('uses the same physical forward-only projection for a zero-factor declaration', () => {
|
|
15
|
+
const material = Materials.standard({
|
|
16
|
+
baseColor: [1, 1, 1, 1],
|
|
17
|
+
clearcoat: 0,
|
|
18
|
+
clearcoatRoughness: 0,
|
|
19
|
+
});
|
|
20
|
+
expect(material.passes?.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
|
|
21
|
+
expect(material.parameters?.some((parameter) => parameter.name === 'clearcoat')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
projectStandardSurfacePasses,
|
|
6
|
+
type StandardSurfaceProjectionOptions,
|
|
7
|
+
} from '../assembly/material/surface-projection';
|
|
8
|
+
import { GPU_DRIVEN_RIGID_UNLIT_WGSL } from '../gpu-driven/production-raster';
|
|
9
|
+
|
|
10
|
+
function shaderSource(name: string): string {
|
|
11
|
+
return readFileSync(
|
|
12
|
+
fileURLToPath(new URL(`../../../shader/src/${name}`, import.meta.url)),
|
|
13
|
+
'utf8',
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const standard: StandardSurfaceProjectionOptions = {
|
|
18
|
+
surfaceModule: 'game_3d::rusted_iron_surface',
|
|
19
|
+
values: {},
|
|
20
|
+
geometryVariant: 'rigid',
|
|
21
|
+
lightingLane: 'direct',
|
|
22
|
+
alphaClip: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('Surface adjacent regressions', () => {
|
|
26
|
+
it('retains the default Standard pass family and provenance', () => {
|
|
27
|
+
const passes = projectStandardSurfacePasses(standard);
|
|
28
|
+
expect(passes.map((pass) => pass.name)).toEqual(['forward', 'deferred', 'shadow-caster']);
|
|
29
|
+
expect(
|
|
30
|
+
passes.every((pass) => pass.program.moduleSlots?.surface === standard.surfaceModule),
|
|
31
|
+
).toBe(true);
|
|
32
|
+
expect(
|
|
33
|
+
passes.every(
|
|
34
|
+
(pass) =>
|
|
35
|
+
(pass.renderState?.tags as Readonly<Record<string, string>> | undefined)?.SurfaceKind ===
|
|
36
|
+
'standard',
|
|
37
|
+
),
|
|
38
|
+
).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('keeps explicit full-custom pass ownership closed', () => {
|
|
42
|
+
const passes = projectStandardSurfacePasses({
|
|
43
|
+
...standard,
|
|
44
|
+
surfaceKind: 'full-custom',
|
|
45
|
+
declaredPasses: ['forward'],
|
|
46
|
+
});
|
|
47
|
+
expect(passes.map((pass) => pass.name)).toEqual(['forward']);
|
|
48
|
+
expect(
|
|
49
|
+
(passes[0]?.renderState?.tags as Readonly<Record<string, string>> | undefined)?.SurfaceKind,
|
|
50
|
+
).toBe('full-custom');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps skinning and GPU-driven siblings on their own shader owners', () => {
|
|
54
|
+
const skinned = shaderSource('default-standard-pbr-skin.wgsl');
|
|
55
|
+
expect(skinned).toContain('skinIndex');
|
|
56
|
+
expect(skinned).toContain('skinWeight');
|
|
57
|
+
expect(GPU_DRIVEN_RIGID_UNLIT_WGSL).toContain('@builtin(instance_index)');
|
|
58
|
+
expect(GPU_DRIVEN_RIGID_UNLIT_WGSL).not.toContain('surface_v1');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { projectStandardSurfacePasses } from '../assembly/material/surface-projection';
|
|
3
|
+
import { layerPlanFor, passFor, SURFACE_CASES } from './surface-standard-pipeline.fixture';
|
|
4
|
+
|
|
5
|
+
describe('Standard Surface render projection contract', () => {
|
|
6
|
+
it('projects every published cell to its declared pass and Surface module', () => {
|
|
7
|
+
for (const surfaceCase of SURFACE_CASES) {
|
|
8
|
+
const passes = projectStandardSurfacePasses({
|
|
9
|
+
surfaceModule: surfaceCase.surfaceModule,
|
|
10
|
+
values: surfaceCase.material.values ?? {},
|
|
11
|
+
geometryVariant: 'rigid',
|
|
12
|
+
lightingLane: 'direct',
|
|
13
|
+
alphaClip: true,
|
|
14
|
+
layerPlan: layerPlanFor(surfaceCase),
|
|
15
|
+
});
|
|
16
|
+
expect(passes.some((entry) => entry.name === passFor(surfaceCase))).toBe(true);
|
|
17
|
+
expect(passes[0]?.program.moduleSlots?.surface).toBe(surfaceCase.surfaceModule);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|