@forgeax/engine-render 0.1.23 → 0.1.24
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 +131 -9
- 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-shader-policy.d.ts +168 -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/authoring.mjs +1 -1
- package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
- package/dist/chunk-GE3SDD72.mjs.map +1 -0
- package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
- package/dist/chunk-KZFTK6DO.mjs.map +1 -0
- package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
- package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
- package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
- package/dist/chunk-RR5L4SRS.mjs.map +1 -0
- package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
- package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
- package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
- package/dist/chunk-UQYS7FRM.mjs.map +1 -0
- package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
- package/dist/chunk-YKB5DQV7.mjs.map +1 -0
- package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
- package/dist/chunk-YRUYGA4A.mjs.map +1 -0
- package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
- package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
- package/dist/construct-renderer.mjs +19841 -16190
- 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 +28 -0
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +1 -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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6 -6
- 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 +34 -10
- 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/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 +5 -0
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +14 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +14 -1
- 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 +26 -4
- package/dist/record/render-context.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 +64 -7
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +12 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -0
- package/dist/render-system-extract.d.ts +156 -22
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +128 -4
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +19 -0
- package/dist/scene/render-scene.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__/ibl/face-uniforms.unit.test.ts +2 -0
- package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
- package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
- 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__/materials-standard-contract.unit.test.ts +104 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- 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 +85 -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__/standard-layer-projection.unit.test.ts +23 -0
- package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
- 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 +81 -10088
- package/src/assembly/material/assembly.ts +18 -2
- package/src/assembly/material/standard-layer-projection.ts +16 -0
- package/src/assembly/material-shader-policy.ts +854 -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 +4079 -0
- package/src/assembly/webgpu-renderer.ts +4095 -0
- package/src/components/camera.ts +1 -1
- 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 +97 -2
- package/src/gpu-scene.ts +3 -2
- package/src/ibl/IblPipelineCache.ts +134 -50
- package/src/ibl/skylight-bind-group.ts +26 -0
- package/src/index.ts +9 -1
- package/src/light-buffer-layout.ts +3 -0
- package/src/materials.ts +262 -45
- package/src/mesh-material-bindings.ts +59 -0
- package/src/occlusion-proxy.ts +64 -0
- package/src/pbr-pipeline.ts +139 -15
- 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 +15 -0
- package/src/record/frame-targets.ts +11 -1
- package/src/record/frame.ts +6 -2
- package/src/record/helpers.ts +1 -1
- package/src/record/main-pass-geometry.ts +230 -5
- package/src/record/main-pass-material.ts +316 -209
- package/src/record/main-pass-sprite-draws.ts +6 -7
- 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 +35 -4
- package/src/record/typed-frame-graph.ts +45 -1
- package/src/record/view-ubo.ts +3 -3
- package/src/recovery/render-system-candidate.ts +839 -0
- package/src/render-contract.ts +65 -7
- package/src/render-system-extract-tail.ts +2983 -0
- package/src/render-system-extract.ts +124 -2977
- package/src/render-system.ts +868 -659
- package/src/scene/render-scene.ts +124 -1
- 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/dist/chunk-4L3P6DT5.mjs.map +0 -1
- 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-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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { RhiError } from '@forgeax/engine-rhi';
|
|
4
|
+
import type { MaterialShaderManifestVariant } from '@forgeax/engine-shader';
|
|
4
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
6
|
import { deviceOptionsForAdapter } from '../assembly/device-feature-admission';
|
|
6
7
|
import {
|
|
7
8
|
allowsUnlitPreparedFallback,
|
|
9
|
+
isSharedMaterialUserRegionCompatible,
|
|
8
10
|
normalizeMaterialShaderVariantSet,
|
|
9
11
|
resolveMaterialShaderBindingContract,
|
|
10
12
|
resolveMaterialShaderVariantSet,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
type PipelineBuilderContext,
|
|
19
21
|
validateTemporalPipelineContract,
|
|
20
22
|
} from '../pipeline-builder';
|
|
23
|
+
import { effectiveMaterialLayoutIdentity } from '../record/main-pass-geometry';
|
|
21
24
|
import { userRegionTextureFieldOrder } from '../record/main-pass-material';
|
|
22
25
|
|
|
23
26
|
const hdrpVariantKey =
|
|
@@ -25,7 +28,10 @@ const hdrpVariantKey =
|
|
|
25
28
|
const urpVariantKey =
|
|
26
29
|
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false';
|
|
27
30
|
|
|
28
|
-
function noColorVariant(
|
|
31
|
+
function noColorVariant(
|
|
32
|
+
definesKey: string,
|
|
33
|
+
clusterForward: boolean,
|
|
34
|
+
): MaterialShaderManifestVariant {
|
|
29
35
|
return {
|
|
30
36
|
definesKey,
|
|
31
37
|
defines: {
|
|
@@ -57,7 +63,7 @@ describe('material shader variant identity', () => {
|
|
|
57
63
|
} as never),
|
|
58
64
|
).toEqual({
|
|
59
65
|
requiredFeatures: [],
|
|
60
|
-
requiredLimits: { maxSampledTexturesPerShaderStage:
|
|
66
|
+
requiredLimits: { maxSampledTexturesPerShaderStage: 21 },
|
|
61
67
|
});
|
|
62
68
|
expect(
|
|
63
69
|
deviceOptionsForAdapter({
|
|
@@ -80,6 +86,17 @@ describe('material shader variant identity', () => {
|
|
|
80
86
|
).toBe(urpVariantKey);
|
|
81
87
|
});
|
|
82
88
|
|
|
89
|
+
it('keeps physical root declarations out of capability variant selection', () => {
|
|
90
|
+
const manifest = noColorManifest();
|
|
91
|
+
expect(
|
|
92
|
+
selectNoColorPbrVariant(
|
|
93
|
+
manifest,
|
|
94
|
+
true,
|
|
95
|
+
`${hdrpVariantKey}+CLEARCOAT_TEXTURE_AVAILABLE=true+TRANSMISSION_AVAILABLE=true`,
|
|
96
|
+
)?.definesKey,
|
|
97
|
+
).toBe(hdrpVariantKey);
|
|
98
|
+
});
|
|
99
|
+
|
|
83
100
|
it('closes the WebGL2 colored PBR axes on the URP variant and layout', () => {
|
|
84
101
|
const variants = [
|
|
85
102
|
{
|
|
@@ -497,13 +514,12 @@ describe('temporal material PSO creation contract', () => {
|
|
|
497
514
|
});
|
|
498
515
|
|
|
499
516
|
describe('material shader capability variant resolution', () => {
|
|
500
|
-
it('
|
|
517
|
+
it('rewrites only backend capability axes', () => {
|
|
501
518
|
const variants = [
|
|
502
519
|
{
|
|
503
520
|
defines: {
|
|
504
521
|
CLUSTER_FORWARD_AVAILABLE: false,
|
|
505
522
|
STORAGE_BUFFER_AVAILABLE: false,
|
|
506
|
-
TRANSMISSION_AVAILABLE: false,
|
|
507
523
|
VERTEX_COLOR_AVAILABLE: false,
|
|
508
524
|
},
|
|
509
525
|
},
|
|
@@ -511,7 +527,6 @@ describe('material shader capability variant resolution', () => {
|
|
|
511
527
|
defines: {
|
|
512
528
|
CLUSTER_FORWARD_AVAILABLE: false,
|
|
513
529
|
STORAGE_BUFFER_AVAILABLE: false,
|
|
514
|
-
TRANSMISSION_AVAILABLE: true,
|
|
515
530
|
VERTEX_COLOR_AVAILABLE: false,
|
|
516
531
|
},
|
|
517
532
|
},
|
|
@@ -524,17 +539,7 @@ describe('material shader capability variant resolution', () => {
|
|
|
524
539
|
false,
|
|
525
540
|
),
|
|
526
541
|
).toBe(
|
|
527
|
-
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false+
|
|
528
|
-
);
|
|
529
|
-
expect(
|
|
530
|
-
resolveMaterialShaderVariantSet(
|
|
531
|
-
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false',
|
|
532
|
-
variants,
|
|
533
|
-
'wgpu-webgl2',
|
|
534
|
-
false,
|
|
535
|
-
),
|
|
536
|
-
).toBe(
|
|
537
|
-
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false+TRANSMISSION_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false',
|
|
542
|
+
'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=false',
|
|
538
543
|
);
|
|
539
544
|
});
|
|
540
545
|
|
|
@@ -560,6 +565,16 @@ describe('material shader capability variant resolution', () => {
|
|
|
560
565
|
});
|
|
561
566
|
|
|
562
567
|
describe('material shader binding contract', () => {
|
|
568
|
+
it('does not reuse the shared PBR layout when compact texture names shift semantics', () => {
|
|
569
|
+
expect(
|
|
570
|
+
isSharedMaterialUserRegionCompatible([
|
|
571
|
+
{ name: 'baseColor', type: 'color' },
|
|
572
|
+
{ name: 'baseColorTexture', type: 'texture2d' },
|
|
573
|
+
{ name: 'normalTexture', type: 'texture2d' },
|
|
574
|
+
]),
|
|
575
|
+
).toBe(false);
|
|
576
|
+
});
|
|
577
|
+
|
|
563
578
|
it('preserves compact authored texture order for custom material schemas', () => {
|
|
564
579
|
expect(
|
|
565
580
|
userRegionTextureFieldOrder([
|
|
@@ -573,7 +588,6 @@ describe('material shader binding contract', () => {
|
|
|
573
588
|
'baseColorTexture',
|
|
574
589
|
'metallicRoughnessTexture',
|
|
575
590
|
'normalTexture',
|
|
576
|
-
'specularTintTexture',
|
|
577
591
|
'emissiveTexture',
|
|
578
592
|
'occlusionTexture',
|
|
579
593
|
'transmissionTexture',
|
|
@@ -650,8 +664,29 @@ describe('material shader binding contract', () => {
|
|
|
650
664
|
});
|
|
651
665
|
|
|
652
666
|
describe('material shader color-domain contract', () => {
|
|
667
|
+
it('keeps shared sprite layouts stable while splitting standard clearcoat maps', () => {
|
|
668
|
+
const sharedSpriteSchema = [{ name: 'tint', type: 'color' as const }];
|
|
669
|
+
const standardClearcoatSchema = [
|
|
670
|
+
{ name: 'clearcoat', type: 'f32' as const },
|
|
671
|
+
{ name: 'clearcoatTexture', type: 'texture2d' as const },
|
|
672
|
+
];
|
|
673
|
+
|
|
674
|
+
expect(effectiveMaterialLayoutIdentity('forgeax::sprite', sharedSpriteSchema)).toBeUndefined();
|
|
675
|
+
expect(
|
|
676
|
+
effectiveMaterialLayoutIdentity('forgeax::default-standard-pbr', standardClearcoatSchema),
|
|
677
|
+
).toBeTypeOf('string');
|
|
678
|
+
expect(
|
|
679
|
+
effectiveMaterialLayoutIdentity('forgeax::default-standard-pbr', [
|
|
680
|
+
{ name: 'clearcoat', type: 'f32' as const },
|
|
681
|
+
]),
|
|
682
|
+
).toBeUndefined();
|
|
683
|
+
});
|
|
684
|
+
|
|
653
685
|
it('routes HDR sprite targets to fs_main_hdr while unorm targets use fs_main', () => {
|
|
654
|
-
const source =
|
|
686
|
+
const source = [
|
|
687
|
+
readFileSync(resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'), 'utf8'),
|
|
688
|
+
readFileSync(resolve(import.meta.dirname, '../assembly/webgpu-ready.ts'), 'utf8'),
|
|
689
|
+
].join('\n');
|
|
655
690
|
expect(source).toContain(
|
|
656
691
|
"isHdr &&\n (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')",
|
|
657
692
|
);
|
|
@@ -659,6 +694,38 @@ describe('material shader color-domain contract', () => {
|
|
|
659
694
|
expect(source).toContain('colorFormat: isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat');
|
|
660
695
|
expect(source).toContain("entryPoint = 'fs_main_hdr'");
|
|
661
696
|
});
|
|
697
|
+
|
|
698
|
+
it('keeps physical map layout derivation in the shared pipeline owner', () => {
|
|
699
|
+
const source = readFileSync(
|
|
700
|
+
resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'),
|
|
701
|
+
'utf8',
|
|
702
|
+
);
|
|
703
|
+
expect(source).toContain('isCanonicalStandardPbrMaterialShader(materialShaderId)');
|
|
704
|
+
expect(source).toContain('requiresStandardMapLayout');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('does not reuse the scalar shared BGL for authored clearcoat maps', () => {
|
|
708
|
+
const source = readFileSync(
|
|
709
|
+
resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'),
|
|
710
|
+
'utf8',
|
|
711
|
+
);
|
|
712
|
+
expect(source).toMatch(
|
|
713
|
+
/if \(\s*!isStandardMapLayout\s*&&\s*isSharedMaterialUserRegionCompatible\(/u,
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('keeps the fixed standard texture ABI before authored clearcoat slots', () => {
|
|
718
|
+
const pipelineSource = readFileSync(resolve(import.meta.dirname, '../pbr-pipeline.ts'), 'utf8');
|
|
719
|
+
const recordSource = readFileSync(
|
|
720
|
+
resolve(import.meta.dirname, '../record/main-pass-material.ts'),
|
|
721
|
+
'utf8',
|
|
722
|
+
);
|
|
723
|
+
expect(pipelineSource).toContain(
|
|
724
|
+
'const physicalFields = physicalTextureFields(effectiveSchema)',
|
|
725
|
+
);
|
|
726
|
+
expect(pipelineSource).toContain('appendTextureInjection(afterTransmission, physicalFields)');
|
|
727
|
+
expect(recordSource).toContain('BUILTIN_USER_REGION_TEXTURE_FIELDS');
|
|
728
|
+
});
|
|
662
729
|
});
|
|
663
730
|
|
|
664
731
|
describe('material shader vertex input contract', () => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createContinuationTerminator,
|
|
4
|
+
executeRendererFrameTransaction,
|
|
5
|
+
} from '../assembly/renderer-frame-transaction';
|
|
6
|
+
|
|
7
|
+
describe('renderer frame generation fence', () => {
|
|
8
|
+
it('rejects a generation that changes between finish and submit', () => {
|
|
9
|
+
const activeOwner = { scope: { generation: 3 } };
|
|
10
|
+
const submitted: string[] = [];
|
|
11
|
+
const result = executeRendererFrameTransaction({
|
|
12
|
+
build: () => ({ ok: true, value: 'command' }),
|
|
13
|
+
execute: () => ({ ok: true, value: undefined }),
|
|
14
|
+
finish: () => {
|
|
15
|
+
activeOwner.scope = { generation: 4 };
|
|
16
|
+
return { ok: true, value: undefined };
|
|
17
|
+
},
|
|
18
|
+
submit: () => {
|
|
19
|
+
submitted.push('stale-command');
|
|
20
|
+
return { ok: true, value: undefined };
|
|
21
|
+
},
|
|
22
|
+
commit: () => undefined,
|
|
23
|
+
generationFence: {
|
|
24
|
+
capturedGeneration: 3,
|
|
25
|
+
currentGeneration: () => activeOwner.scope.generation,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(result).toEqual({ ok: false, error: { stage: 'submit' } });
|
|
30
|
+
expect(submitted).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('terminates completion and all async continuations exactly once', async () => {
|
|
34
|
+
const terminator = createContinuationTerminator();
|
|
35
|
+
const completion = terminator.promise();
|
|
36
|
+
|
|
37
|
+
expect(terminator.terminate({ code: 'device-lost' })).toBe(true);
|
|
38
|
+
expect(terminator.terminate({ code: 'disposed' })).toBe(false);
|
|
39
|
+
expect(terminator.isTerminated()).toBe(true);
|
|
40
|
+
await expect(completion).resolves.toEqual({ code: 'device-lost' });
|
|
41
|
+
|
|
42
|
+
expect(terminator.guard('readback')).toBe(false);
|
|
43
|
+
expect(terminator.guard('query')).toBe(false);
|
|
44
|
+
expect(terminator.guard('mapAsync')).toBe(false);
|
|
45
|
+
expect(terminator.guard('queue-completion')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildGenerationAggregate,
|
|
4
|
+
createGenerationAllocator,
|
|
5
|
+
type GenerationAggregate,
|
|
6
|
+
type GenerationPublication,
|
|
7
|
+
publishGeneration,
|
|
8
|
+
} from '../assembly/recovery/generation';
|
|
9
|
+
import { DeviceScope, type LifecycleResourceSpec } from '../device/device-scope';
|
|
10
|
+
|
|
11
|
+
type TestAggregate = GenerationAggregate<
|
|
12
|
+
{ readonly id: string },
|
|
13
|
+
{ readonly id: string },
|
|
14
|
+
{ readonly id: string },
|
|
15
|
+
{ readonly id: string },
|
|
16
|
+
{ readonly id: string }
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
function root(
|
|
20
|
+
kind: LifecycleResourceSpec<unknown>['kind'],
|
|
21
|
+
order: string[],
|
|
22
|
+
): LifecycleResourceSpec<unknown> {
|
|
23
|
+
return {
|
|
24
|
+
kind,
|
|
25
|
+
create: () => {
|
|
26
|
+
order.push(`create:${kind}`);
|
|
27
|
+
return { kind };
|
|
28
|
+
},
|
|
29
|
+
cleanup: () => {
|
|
30
|
+
order.push(`cleanup:${kind}`);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('generation assembly', () => {
|
|
36
|
+
it('keeps the candidate detached and publishes one immutable aggregate', async () => {
|
|
37
|
+
const order: string[] = [];
|
|
38
|
+
const scope = DeviceScope.create(4, 'renderer');
|
|
39
|
+
const candidate = await buildGenerationAggregate<TestAggregate>({
|
|
40
|
+
scope,
|
|
41
|
+
device: { id: 'device-4' },
|
|
42
|
+
context: { id: 'context-4' },
|
|
43
|
+
pipeline: { id: 'pipeline-4' },
|
|
44
|
+
producerBindings: { id: 'producers-4' },
|
|
45
|
+
graph: { id: 'graph-4' },
|
|
46
|
+
roots: [root('surface', order), root('pipeline', order)],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(candidate.ok).toBe(true);
|
|
50
|
+
if (!candidate.ok) return;
|
|
51
|
+
|
|
52
|
+
const publication: GenerationPublication<TestAggregate | undefined> = {
|
|
53
|
+
current: undefined,
|
|
54
|
+
};
|
|
55
|
+
expect(publication.current).toBeUndefined();
|
|
56
|
+
expect(Object.isFrozen(candidate.value)).toBe(true);
|
|
57
|
+
expect(candidate.value.generation).toBe(4);
|
|
58
|
+
expect(candidate.value.scope).toBe(scope);
|
|
59
|
+
expect(order).toEqual(['create:surface', 'create:pipeline']);
|
|
60
|
+
|
|
61
|
+
publishGeneration(publication, candidate.value, (value) => value.scope.isAlive());
|
|
62
|
+
|
|
63
|
+
expect(publication.current).toBe(candidate.value);
|
|
64
|
+
expect(publication.current?.device.id).toBe('device-4');
|
|
65
|
+
expect(publication.current?.context.id).toBe('context-4');
|
|
66
|
+
expect(publication.current?.pipeline.id).toBe('pipeline-4');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('allocates each candidate generation exactly once without moving the active generation', () => {
|
|
70
|
+
const allocator = createGenerationAllocator(9);
|
|
71
|
+
|
|
72
|
+
expect(allocator.next()).toBe(10);
|
|
73
|
+
expect(allocator.next()).toBe(11);
|
|
74
|
+
expect(allocator.next()).toBe(12);
|
|
75
|
+
expect(allocator.activeGeneration).toBe(9);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildGenerationAggregate,
|
|
4
|
+
type GenerationAggregate,
|
|
5
|
+
} from '../assembly/recovery/generation';
|
|
6
|
+
import { DeviceScope, type LifecycleResourceSpec } from '../device/device-scope';
|
|
7
|
+
|
|
8
|
+
type FailureAggregate = GenerationAggregate<
|
|
9
|
+
{ readonly id: string },
|
|
10
|
+
{ readonly id: string },
|
|
11
|
+
{ readonly id: string },
|
|
12
|
+
{ readonly id: string },
|
|
13
|
+
{ readonly id: string }
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
const kinds: LifecycleResourceSpec<unknown>['kind'][] = [
|
|
17
|
+
'surface',
|
|
18
|
+
'pipeline',
|
|
19
|
+
'buffer',
|
|
20
|
+
'texture',
|
|
21
|
+
'feature',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function root(
|
|
25
|
+
kind: LifecycleResourceSpec<unknown>['kind'],
|
|
26
|
+
events: string[],
|
|
27
|
+
failure: boolean,
|
|
28
|
+
): LifecycleResourceSpec<unknown> {
|
|
29
|
+
return {
|
|
30
|
+
kind,
|
|
31
|
+
create: () => {
|
|
32
|
+
events.push(`create:${kind}`);
|
|
33
|
+
if (failure) throw new Error(`failure:${kind}`);
|
|
34
|
+
return { kind };
|
|
35
|
+
},
|
|
36
|
+
cleanup: () => {
|
|
37
|
+
events.push(`cleanup:${kind}`);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function aggregate(scope: DeviceScope): FailureAggregate {
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
generation: scope.generation,
|
|
45
|
+
scope,
|
|
46
|
+
device: { id: `device-${scope.generation}` },
|
|
47
|
+
context: { id: `context-${scope.generation}` },
|
|
48
|
+
pipeline: { id: `pipeline-${scope.generation}` },
|
|
49
|
+
producerBindings: { id: `producers-${scope.generation}` },
|
|
50
|
+
graph: { id: `graph-${scope.generation}` },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('generation assembly failure matrix', () => {
|
|
55
|
+
it.each(kinds)('abandons a candidate when the %s root fails', async (failedKind) => {
|
|
56
|
+
const events: string[] = [];
|
|
57
|
+
const activeScope = DeviceScope.create(9, 'renderer');
|
|
58
|
+
const active = aggregate(activeScope);
|
|
59
|
+
const candidateScope = DeviceScope.create(11, 'renderer');
|
|
60
|
+
const result = await buildGenerationAggregate<FailureAggregate>({
|
|
61
|
+
scope: candidateScope,
|
|
62
|
+
device: { id: 'device-11' },
|
|
63
|
+
context: { id: 'context-11' },
|
|
64
|
+
pipeline: { id: 'pipeline-11' },
|
|
65
|
+
producerBindings: { id: 'producers-11' },
|
|
66
|
+
graph: { id: 'graph-11' },
|
|
67
|
+
roots: kinds.map((kind) => root(kind, events, kind === failedKind)),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(result.ok).toBe(false);
|
|
71
|
+
if (result.ok) return;
|
|
72
|
+
expect(result.error.code).toBe('generation-assembly-failed');
|
|
73
|
+
expect(result.error.generation).toBe(11);
|
|
74
|
+
expect(result.error.resourceKind).toBe(failedKind);
|
|
75
|
+
expect(result.error.cleanupFailures).toEqual([]);
|
|
76
|
+
expect(candidateScope.state).toBe('abandoned');
|
|
77
|
+
expect(events.filter((event) => event.startsWith('cleanup:'))).toEqual(
|
|
78
|
+
events
|
|
79
|
+
.filter((event) => event.startsWith('create:') && !event.endsWith(failedKind))
|
|
80
|
+
.reverse()
|
|
81
|
+
.map((event) => event.replace('create:', 'cleanup:')),
|
|
82
|
+
);
|
|
83
|
+
expect(active.scope.state).toBe('active');
|
|
84
|
+
expect(active.generation).toBe(9);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('keeps the active aggregate unchanged when candidate publication is skipped', async () => {
|
|
88
|
+
const events: string[] = [];
|
|
89
|
+
const activeScope = DeviceScope.create(20, 'renderer');
|
|
90
|
+
const active = aggregate(activeScope);
|
|
91
|
+
const candidateScope = DeviceScope.create(22, 'renderer');
|
|
92
|
+
const result = await buildGenerationAggregate<FailureAggregate>({
|
|
93
|
+
scope: candidateScope,
|
|
94
|
+
device: { id: 'device-22' },
|
|
95
|
+
context: { id: 'context-22' },
|
|
96
|
+
pipeline: { id: 'pipeline-22' },
|
|
97
|
+
producerBindings: { id: 'producers-22' },
|
|
98
|
+
graph: { id: 'graph-22' },
|
|
99
|
+
roots: [root('surface', events, false), root('pipeline', events, true)],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.ok).toBe(false);
|
|
103
|
+
expect(active).toEqual(aggregate(activeScope));
|
|
104
|
+
expect(active.scope.state).toBe('active');
|
|
105
|
+
expect(candidateScope.generation).toBe(22);
|
|
106
|
+
expect(events).toEqual(['create:surface', 'create:pipeline', 'cleanup:surface']);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createRendererProducerRootMatrix } from '../render-system';
|
|
3
|
+
|
|
4
|
+
describe('renderer producer root matrix', () => {
|
|
5
|
+
it('enumerates the current baseline resource families with one owner each', () => {
|
|
6
|
+
const matrix = createRendererProducerRootMatrix();
|
|
7
|
+
|
|
8
|
+
expect(matrix.map((root) => root.kind)).toEqual([
|
|
9
|
+
'backend-surface',
|
|
10
|
+
'shader-material-pipeline',
|
|
11
|
+
'mesh-texture-sampler',
|
|
12
|
+
'render-scene',
|
|
13
|
+
'gpu-driven',
|
|
14
|
+
'feature',
|
|
15
|
+
'external-source',
|
|
16
|
+
'render-graph',
|
|
17
|
+
'target-history',
|
|
18
|
+
'observation-lease',
|
|
19
|
+
]);
|
|
20
|
+
expect(new Set(matrix.map((root) => root.owner)).size).toBe(matrix.length);
|
|
21
|
+
expect(matrix.every((root) => root.candidateScope === 'device-scope')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('separates visible roots from lazy non-visible work', () => {
|
|
25
|
+
const matrix = createRendererProducerRootMatrix();
|
|
26
|
+
const visible = matrix.filter((root) => root.visibility === 'visible-workset');
|
|
27
|
+
const lazy = matrix.filter((root) => root.visibility === 'non-visible-lazy');
|
|
28
|
+
|
|
29
|
+
expect(visible.length).toBeGreaterThan(0);
|
|
30
|
+
expect(lazy.length).toBeGreaterThan(0);
|
|
31
|
+
expect(lazy.every((root) => root.disabledWork === 'zero')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -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
|
+
});
|