@forgeax/engine-render 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +379 -33
- package/dist/assembly/analytic-fog-registration.d.ts +3 -0
- package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
- package/dist/assembly/fxaa-registration.d.ts +4 -0
- package/dist/assembly/fxaa-registration.d.ts.map +1 -0
- package/dist/assembly/lod-observation.d.ts +4 -0
- package/dist/assembly/lod-observation.d.ts.map +1 -0
- package/dist/assembly/motion-blur-registration.d.ts +3 -0
- package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
- package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/bloom-admission.d.ts +13 -0
- package/dist/bloom-admission.d.ts.map +1 -0
- package/dist/{chunk-DRR7D3KX.mjs → chunk-3RTCJTH4.mjs} +12 -9
- package/dist/chunk-3RTCJTH4.mjs.map +1 -0
- package/dist/{chunk-S665NJNF.mjs → chunk-LEAXOMYJ.mjs} +3 -3
- package/dist/{chunk-S665NJNF.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
- package/dist/{chunk-6TFBXLRJ.mjs → chunk-NB2S7ZRX.mjs} +155 -11
- package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
- package/dist/{chunk-LULCA3OF.mjs → chunk-NNQL7CB3.mjs} +4 -4
- package/dist/{chunk-LULCA3OF.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
- package/dist/{chunk-4NNZQCL3.mjs → chunk-OO3DRYK7.mjs} +66 -200
- package/dist/chunk-OO3DRYK7.mjs.map +1 -0
- package/dist/{chunk-GWCEMDVR.mjs → chunk-PLAXOOX5.mjs} +232 -7
- package/dist/chunk-PLAXOOX5.mjs.map +1 -0
- package/dist/{chunk-JRIPCMYC.mjs → chunk-VOZWDM3L.mjs} +283 -193
- package/dist/chunk-VOZWDM3L.mjs.map +1 -0
- package/dist/{chunk-JCIRYQ4V.mjs → chunk-ZDQWGQW7.mjs} +10 -4
- package/dist/chunk-ZDQWGQW7.mjs.map +1 -0
- package/dist/components/atmosphere.d.ts +2 -0
- package/dist/components/atmosphere.d.ts.map +1 -1
- package/dist/components/camera.d.ts +20 -4
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/mesh-filter.d.ts +6 -5
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/motion-blur.d.ts +1 -0
- package/dist/components/motion-blur.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +2865 -526
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +6 -0
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/analytic-fog.d.ts +6 -0
- package/dist/environment/analytic-fog.d.ts.map +1 -0
- package/dist/errors/render.d.ts +6 -2
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +7 -0
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/extract/environment.d.ts +2 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/features/error-reporting.d.ts +13 -0
- package/dist/features/error-reporting.d.ts.map +1 -0
- package/dist/features/host.d.ts +5 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
- package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
- package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
- package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
- package/dist/features/plan.d.ts +7 -4
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +9 -5
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +7 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +3 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/view-gpu.d.ts +9 -0
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +6 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/inspection-types.d.ts +43 -2
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances.d.ts +8 -1
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +6 -6
- package/dist/pipeline/standard-post.d.ts +28 -19
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +2 -2
- package/dist/record/frame-snapshot.d.ts +3 -0
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +5 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +6 -5
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +12 -28
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +5 -3
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +24 -4
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +3 -1
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +8 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +7 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +2 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +7 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/temporal/view.d.ts +6 -1
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/typed-render-graph-primitives.d.ts +10 -7
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +0 -1
- package/dist/volume/component.d.ts.map +1 -1
- package/dist/volume/passes.d.ts.map +1 -1
- package/dist/volume/temporal.d.ts +3 -1
- package/dist/volume/temporal.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
- package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
- package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
- package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
- package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
- package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
- package/src/__tests__/environment-selection.unit.test.ts +12 -0
- package/src/__tests__/factory-contract.integration.test.ts +331 -68
- package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
- package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
- package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
- package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
- package/src/__tests__/gpu-scene.unit.test.ts +91 -0
- package/src/__tests__/instances-store.unit.test.ts +75 -0
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
- package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
- package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
- package/src/__tests__/public-surface.test-d.ts +8 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
- package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
- package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
- package/src/__tests__/shader-manifest-fixture.ts +4 -4
- package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
- package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
- package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
- package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
- package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
- package/src/__tests__/temporal-view.unit.test.ts +2 -1
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
- package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
- package/src/assembly/analytic-fog-registration.ts +14 -0
- package/src/assembly/fxaa-registration.ts +17 -0
- package/src/assembly/lod-observation.ts +12 -0
- package/src/assembly/motion-blur-registration.ts +17 -0
- package/src/assembly/renderer-facade.ts +2 -0
- package/src/assembly/temporal-post-process-prewarm.ts +40 -0
- package/src/assembly/webgpu-ready-contract.ts +8 -3
- package/src/assembly/webgpu-ready.ts +181 -245
- package/src/assembly/webgpu-renderer.ts +16 -31
- package/src/bloom-admission.ts +21 -0
- package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
- package/src/components/atmosphere.ts +4 -0
- package/src/components/camera.ts +66 -10
- package/src/components/mesh-filter.ts +11 -9
- package/src/components/motion-blur.ts +1 -0
- package/src/device/gpu-residency.ts +56 -12
- package/src/environment/analytic-fog.ts +44 -0
- package/src/environment/background.ts +2 -2
- package/src/errors/render.ts +14 -1
- package/src/extract/camera.ts +20 -7
- package/src/extract/contracts.ts +7 -0
- package/src/extract/environment.ts +4 -0
- package/src/features/error-reporting.ts +78 -0
- package/src/features/host.ts +25 -12
- package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
- package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
- package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
- package/src/features/motion-blur/motion-blur-params.ts +70 -2
- package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
- package/src/features/plan.ts +25 -8
- package/src/features/prepared-gpu-work.ts +86 -35
- package/src/features/render-graph-compute.ts +13 -5
- package/src/features/render-graph-contribution.ts +2 -0
- package/src/gpu-driven/material-artifacts.ts +2 -7
- package/src/gpu-driven/prepared-draw.ts +15 -10
- package/src/gpu-driven/production-raster.ts +83 -7
- package/src/gpu-driven/view-gpu.ts +17 -0
- package/src/gpu-scene.ts +188 -21
- package/src/index.ts +32 -0
- package/src/inspection-types.ts +60 -2
- package/src/instances.ts +80 -1
- package/src/pipeline/standard-post.ts +307 -78
- package/src/pipeline-spec.ts +5 -5
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
- package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
- package/src/record/frame-snapshot.ts +5 -1
- package/src/record/frame-targets.ts +10 -6
- package/src/record/frame.ts +163 -22
- package/src/record/main-pass-geometry.ts +30 -1
- package/src/record/main-pass-sprite-draws.ts +6 -1
- package/src/record/mesh-ssbo.ts +43 -6
- package/src/record/render-context.ts +12 -28
- package/src/record/shadow-pass.ts +6 -1
- package/src/record/skybox-post-pass.ts +122 -196
- package/src/record/typed-frame-graph.ts +153 -9
- package/src/recovery/render-system-candidate.ts +22 -10
- package/src/render-contract.ts +23 -4
- package/src/render-pipeline.ts +6 -1
- package/src/render-system-extract-tail.ts +16 -0
- package/src/render-system-extract.ts +45 -13
- package/src/render-system.ts +255 -120
- package/src/scene/render-scene.ts +85 -4
- package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
- package/src/temporal/frame-coordinator.ts +8 -1
- package/src/temporal/frame.ts +20 -0
- package/src/temporal/view.ts +13 -2
- package/src/typed-render-graph-primitives.ts +121 -48
- package/src/volume/component.ts +0 -1
- package/src/volume/passes.ts +8 -4
- package/src/volume/resources.ts +1 -1
- package/src/volume/temporal.ts +11 -0
- package/dist/chunk-4NNZQCL3.mjs.map +0 -1
- package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
- package/dist/chunk-DRR7D3KX.mjs.map +0 -1
- package/dist/chunk-GWCEMDVR.mjs.map +0 -1
- package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
- package/dist/chunk-JRIPCMYC.mjs.map +0 -1
|
@@ -83,6 +83,10 @@ async function createBloomScenario() {
|
|
|
83
83
|
antialias: ANTIALIAS_NONE,
|
|
84
84
|
tonemap: TONEMAP_ACES_FILMIC,
|
|
85
85
|
bloom: BLOOM_ENABLED,
|
|
86
|
+
bloomThreshold: 1,
|
|
87
|
+
bloomIntensity: 1,
|
|
88
|
+
bloomSoftKnee: 0.5,
|
|
89
|
+
bloomScatter: 0.7,
|
|
86
90
|
},
|
|
87
91
|
},
|
|
88
92
|
)
|
|
@@ -103,10 +107,61 @@ async function createBloomScenario() {
|
|
|
103
107
|
const updated = world.update();
|
|
104
108
|
if (!updated.ok) throw updated.error;
|
|
105
109
|
};
|
|
110
|
+
const setBloomIntensity = (bloomIntensity: number) => {
|
|
111
|
+
world.set(camera, LocalCamera, { bloomIntensity }).unwrap();
|
|
112
|
+
const updated = world.update();
|
|
113
|
+
if (!updated.ok) throw updated.error;
|
|
114
|
+
};
|
|
106
115
|
const dispose = async () => {
|
|
107
116
|
await renderer.dispose();
|
|
108
117
|
};
|
|
109
|
-
return { renderer, draw, setBloom, dispose, canvas };
|
|
118
|
+
return { renderer, draw, setBloom, setBloomIntensity, dispose, canvas };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function createMotionBlurScenario() {
|
|
122
|
+
const renderer = await constructRenderer(
|
|
123
|
+
{ width: 64, height: 64, getContext: () => null },
|
|
124
|
+
{ rhi },
|
|
125
|
+
{ shaderManifestUrl: renderLifecycleManifestUrl() },
|
|
126
|
+
);
|
|
127
|
+
if (!(await renderer.initialization).ok)
|
|
128
|
+
throw new Error('Motion Blur renderer initialization failed');
|
|
129
|
+
const world = new World();
|
|
130
|
+
const attached = renderer.attach(world);
|
|
131
|
+
if (!attached.ok) throw attached.error;
|
|
132
|
+
registerPropagateTransforms(world);
|
|
133
|
+
const camera = world
|
|
134
|
+
.spawn(
|
|
135
|
+
{ component: Transform, data: { pos: [0, 0, 4] } },
|
|
136
|
+
{
|
|
137
|
+
component: LocalCamera,
|
|
138
|
+
data: {
|
|
139
|
+
fov: 1,
|
|
140
|
+
aspect: 1,
|
|
141
|
+
near: 0.1,
|
|
142
|
+
far: 100,
|
|
143
|
+
antialias: ANTIALIAS_NONE,
|
|
144
|
+
tonemap: TONEMAP_ACES_FILMIC,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
.unwrap();
|
|
149
|
+
world
|
|
150
|
+
.addComponent(camera, {
|
|
151
|
+
component: MotionBlur,
|
|
152
|
+
data: { shutterAngle: 180, maxRadiusPixels: 8, sampleCount: 4, targetFps: 60 },
|
|
153
|
+
})
|
|
154
|
+
.unwrap();
|
|
155
|
+
const frameInput = () => ({
|
|
156
|
+
leases: [attached.value],
|
|
157
|
+
camera: { lease: attached.value },
|
|
158
|
+
environment: { lease: attached.value },
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
renderer,
|
|
162
|
+
draw: () => renderer.draw(frameInput()),
|
|
163
|
+
dispose: () => renderer.dispose(),
|
|
164
|
+
};
|
|
110
165
|
}
|
|
111
166
|
|
|
112
167
|
describe('factory contract', () => {
|
|
@@ -1086,14 +1141,111 @@ describe('factory contract', () => {
|
|
|
1086
1141
|
}
|
|
1087
1142
|
const inspection = renderer.inspect();
|
|
1088
1143
|
expect(inspection.perFramePassNames).toContain('motion-blur');
|
|
1144
|
+
// The same Rhi device/frame admitted the compute plan, graph, and both
|
|
1145
|
+
// production dispatches. This is the application-side admission proof;
|
|
1146
|
+
// Dawn/physical timing remains a separate backend gate.
|
|
1147
|
+
expect(inspection.capabilities).toMatchObject({
|
|
1148
|
+
compute: true,
|
|
1149
|
+
storageBuffer: true,
|
|
1150
|
+
storageTexture: true,
|
|
1151
|
+
rgba16floatRenderable: true,
|
|
1152
|
+
});
|
|
1153
|
+
expect(inspection.motionBlur).toMatchObject({ lane: 'compute', passCount: 2 });
|
|
1154
|
+
expect(inspection.perFramePassNames).toContain('motion-blur');
|
|
1155
|
+
expect(inspection.recoveryEvidence.graph).toMatchObject({ ready: true });
|
|
1156
|
+
expect(inspection.featureGraph?.compileSuccesses ?? 0).toBeGreaterThan(0);
|
|
1157
|
+
// The graph is present, but this is the first accepted temporal pair
|
|
1158
|
+
// after the effect was added. Inspection must expose the reset boundary
|
|
1159
|
+
// until a subsequent successful pair can provide a valid baseline.
|
|
1089
1160
|
expect(inspection.motionBlur).toMatchObject({
|
|
1090
1161
|
enabled: true,
|
|
1091
|
-
status: '
|
|
1162
|
+
status: 'reset',
|
|
1092
1163
|
temporalDemand: 'scene-data-temporal-v1',
|
|
1093
1164
|
});
|
|
1094
1165
|
await renderer.dispose();
|
|
1095
1166
|
});
|
|
1096
1167
|
|
|
1168
|
+
it('publishes invalid MotionBlur parameters through renderer inspection', async () => {
|
|
1169
|
+
const renderer = await constructRenderer(
|
|
1170
|
+
{ width: 64, height: 64, getContext: () => null },
|
|
1171
|
+
{ rhi },
|
|
1172
|
+
{ shaderManifestUrl: renderLifecycleManifestUrl() },
|
|
1173
|
+
);
|
|
1174
|
+
expect((await renderer.initialization).ok).toBe(true);
|
|
1175
|
+
const errors: unknown[] = [];
|
|
1176
|
+
renderer.onError((error) => errors.push(error));
|
|
1177
|
+
const world = new World();
|
|
1178
|
+
const attached = renderer.attach(world);
|
|
1179
|
+
expect(attached.ok).toBe(true);
|
|
1180
|
+
if (!attached.ok) return;
|
|
1181
|
+
registerPropagateTransforms(world);
|
|
1182
|
+
const camera = world
|
|
1183
|
+
.spawn(
|
|
1184
|
+
{ component: Transform, data: { pos: [0, 0, 4] } },
|
|
1185
|
+
{
|
|
1186
|
+
component: LocalCamera,
|
|
1187
|
+
data: { fov: 1, aspect: 1, near: 0.1, far: 100, antialias: ANTIALIAS_TAA },
|
|
1188
|
+
},
|
|
1189
|
+
{ component: MotionBlur, data: { targetFps: 59.5 } },
|
|
1190
|
+
)
|
|
1191
|
+
.unwrap();
|
|
1192
|
+
const frameInput = () => ({
|
|
1193
|
+
leases: [attached.value],
|
|
1194
|
+
camera: { lease: attached.value },
|
|
1195
|
+
environment: { lease: attached.value },
|
|
1196
|
+
});
|
|
1197
|
+
expect(world.update().ok).toBe(true);
|
|
1198
|
+
|
|
1199
|
+
const draw = renderer.draw(frameInput());
|
|
1200
|
+
expect(draw.ok).toBe(false);
|
|
1201
|
+
if (!draw.ok) {
|
|
1202
|
+
expect(draw.error).toMatchObject({
|
|
1203
|
+
code: 'motion-blur-invalid-params',
|
|
1204
|
+
detail: { field: 'targetFps', value: 59.5, integer: true },
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
expect(errors).toEqual(
|
|
1208
|
+
expect.arrayContaining([expect.objectContaining({ code: 'motion-blur-invalid-params' })]),
|
|
1209
|
+
);
|
|
1210
|
+
expect(renderer.inspect().motionBlur).toMatchObject({
|
|
1211
|
+
enabled: true,
|
|
1212
|
+
status: 'invalid',
|
|
1213
|
+
lastFailure: 'invalid-params',
|
|
1214
|
+
temporalDemand: 'scene-data-temporal-v1',
|
|
1215
|
+
});
|
|
1216
|
+
await renderer.dispose();
|
|
1217
|
+
void camera;
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it('projects a rejected Motion Blur submit while retaining its accepted baseline', async () => {
|
|
1221
|
+
const scenario = await createMotionBlurScenario();
|
|
1222
|
+
const submit = vi.spyOn(RhiNullQueue.prototype, 'submit');
|
|
1223
|
+
const failure = new RhiError({
|
|
1224
|
+
code: 'webgpu-runtime-error',
|
|
1225
|
+
expected: 'Motion Blur queue submission succeeds',
|
|
1226
|
+
hint: 'repair the queue and retry the same frame',
|
|
1227
|
+
});
|
|
1228
|
+
try {
|
|
1229
|
+
expect(scenario.draw().ok).toBe(true);
|
|
1230
|
+
expect(scenario.draw().ok).toBe(true);
|
|
1231
|
+
const accepted = scenario.renderer.inspect().motionBlur;
|
|
1232
|
+
expect(accepted).toMatchObject({ enabled: true });
|
|
1233
|
+
expect(accepted).not.toHaveProperty('lastFailure');
|
|
1234
|
+
submit.mockReturnValueOnce(err(failure));
|
|
1235
|
+
expect(scenario.draw().ok).toBe(false);
|
|
1236
|
+
expect(scenario.renderer.inspect().motionBlur).toMatchObject({
|
|
1237
|
+
enabled: true,
|
|
1238
|
+
lastFailure: 'submit-failure',
|
|
1239
|
+
});
|
|
1240
|
+
submit.mockRestore();
|
|
1241
|
+
expect(scenario.draw().ok).toBe(true);
|
|
1242
|
+
expect(scenario.renderer.inspect().motionBlur?.lastFailure).toBeUndefined();
|
|
1243
|
+
} finally {
|
|
1244
|
+
submit.mockRestore();
|
|
1245
|
+
await scenario.dispose();
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1097
1249
|
it('retires factory-owned TAA textures and params on an off submit', async () => {
|
|
1098
1250
|
const created = new Map<object, string>();
|
|
1099
1251
|
const destroyed = new Set<object>();
|
|
@@ -1230,12 +1382,12 @@ describe('factory contract', () => {
|
|
|
1230
1382
|
const onInspection = renderer.inspect().bloom;
|
|
1231
1383
|
expect(onInspection.state).toBe('active');
|
|
1232
1384
|
expect(onInspection.graphStatus).toBe('valid');
|
|
1233
|
-
expect(onInspection.targetCount).toBe(
|
|
1234
|
-
expect(onInspection.targetBytes).toBe(
|
|
1235
|
-
expect(onInspection.passCount).toBe(
|
|
1236
|
-
expect(onInspection.uploadCount).toBe(
|
|
1237
|
-
expect(onInspection.bindGroupCount).toBe(
|
|
1238
|
-
expect(onInspection.encodeCount).toBe(
|
|
1385
|
+
expect(onInspection.targetCount).toBe(10);
|
|
1386
|
+
expect(onInspection.targetBytes).toBe(54_560);
|
|
1387
|
+
expect(onInspection.passCount).toBe(10);
|
|
1388
|
+
expect(onInspection.uploadCount).toBe(10);
|
|
1389
|
+
expect(onInspection.bindGroupCount).toBe(10);
|
|
1390
|
+
expect(onInspection.encodeCount).toBe(10);
|
|
1239
1391
|
expect(JSON.parse(JSON.stringify(onInspection))).toEqual(onInspection);
|
|
1240
1392
|
const firstGeneration = onInspection.generation;
|
|
1241
1393
|
expect(firstGeneration).toBeGreaterThan(0);
|
|
@@ -1266,10 +1418,10 @@ describe('factory contract', () => {
|
|
|
1266
1418
|
const recoveredInspection = renderer.inspect().bloom;
|
|
1267
1419
|
expect(recoveredInspection.state).toBe('active');
|
|
1268
1420
|
expect(recoveredInspection.resourceCount).toBeGreaterThan(0);
|
|
1269
|
-
expect(recoveredInspection.passCount).toBe(
|
|
1270
|
-
expect(recoveredInspection.uploadCount).toBe(
|
|
1271
|
-
expect(recoveredInspection.bindGroupCount).toBe(
|
|
1272
|
-
expect(recoveredInspection.encodeCount).toBe(
|
|
1421
|
+
expect(recoveredInspection.passCount).toBe(10);
|
|
1422
|
+
expect(recoveredInspection.uploadCount).toBe(10);
|
|
1423
|
+
expect(recoveredInspection.bindGroupCount).toBe(10);
|
|
1424
|
+
expect(recoveredInspection.encodeCount).toBe(10);
|
|
1273
1425
|
expect(recoveredInspection.generation).toBeGreaterThan(firstGeneration);
|
|
1274
1426
|
await Promise.resolve();
|
|
1275
1427
|
await Promise.resolve();
|
|
@@ -1290,25 +1442,25 @@ describe('factory contract', () => {
|
|
|
1290
1442
|
try {
|
|
1291
1443
|
expect(scenario.draw().ok).toBe(true);
|
|
1292
1444
|
expect(scenario.renderer.inspect().bloom).toMatchObject({
|
|
1293
|
-
uploadCount:
|
|
1294
|
-
bindGroupCount:
|
|
1295
|
-
encodeCount:
|
|
1445
|
+
uploadCount: 10,
|
|
1446
|
+
bindGroupCount: 10,
|
|
1447
|
+
encodeCount: 10,
|
|
1296
1448
|
});
|
|
1297
1449
|
|
|
1298
1450
|
submit.mockReturnValueOnce(err(failure));
|
|
1299
1451
|
expect(scenario.draw().ok).toBe(false);
|
|
1300
1452
|
expect(scenario.renderer.inspect().bloom).toMatchObject({
|
|
1301
1453
|
state: 'active',
|
|
1302
|
-
uploadCount:
|
|
1303
|
-
bindGroupCount:
|
|
1304
|
-
encodeCount:
|
|
1454
|
+
uploadCount: 10,
|
|
1455
|
+
bindGroupCount: 10,
|
|
1456
|
+
encodeCount: 10,
|
|
1305
1457
|
});
|
|
1306
1458
|
|
|
1307
1459
|
expect(scenario.draw().ok).toBe(true);
|
|
1308
1460
|
expect(scenario.renderer.inspect().bloom).toMatchObject({
|
|
1309
|
-
uploadCount:
|
|
1310
|
-
bindGroupCount:
|
|
1311
|
-
encodeCount:
|
|
1461
|
+
uploadCount: 10,
|
|
1462
|
+
bindGroupCount: 10,
|
|
1463
|
+
encodeCount: 10,
|
|
1312
1464
|
});
|
|
1313
1465
|
} finally {
|
|
1314
1466
|
submit.mockRestore();
|
|
@@ -1316,6 +1468,26 @@ describe('factory contract', () => {
|
|
|
1316
1468
|
}
|
|
1317
1469
|
});
|
|
1318
1470
|
|
|
1471
|
+
it('uploads positive Bloom parameter changes without rebuilding the accepted bundle', async () => {
|
|
1472
|
+
const scenario = await createBloomScenario();
|
|
1473
|
+
try {
|
|
1474
|
+
expect(scenario.draw().ok).toBe(true);
|
|
1475
|
+
const first = scenario.renderer.inspect().bloom;
|
|
1476
|
+
scenario.setBloomIntensity(0.35);
|
|
1477
|
+
expect(scenario.draw().ok).toBe(true);
|
|
1478
|
+
const updated = scenario.renderer.inspect().bloom;
|
|
1479
|
+
expect(updated.state).toBe('active');
|
|
1480
|
+
expect(updated.generation).toBe(first.generation);
|
|
1481
|
+
expect(updated.resourceCount).toBe(first.resourceCount);
|
|
1482
|
+
expect(updated.targetCount).toBe(first.targetCount);
|
|
1483
|
+
expect(updated.targetBytes).toBe(first.targetBytes);
|
|
1484
|
+
expect(updated.uploadCount).toBe(10);
|
|
1485
|
+
expect(updated.passCount).toBe(10);
|
|
1486
|
+
} finally {
|
|
1487
|
+
await scenario.dispose();
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1319
1491
|
it('aborts a Bloom pass write failure without publishing partial receipts', async () => {
|
|
1320
1492
|
const originalWriteBuffer = RhiNullQueue.prototype.writeBuffer;
|
|
1321
1493
|
const createBuffer = vi.spyOn(RhiNullDevice.prototype, 'createBuffer');
|
|
@@ -1324,30 +1496,30 @@ describe('factory contract', () => {
|
|
|
1324
1496
|
const scenario = await createBloomScenario();
|
|
1325
1497
|
const failure = new RhiError({
|
|
1326
1498
|
code: 'webgpu-runtime-error',
|
|
1327
|
-
expected: 'Bloom
|
|
1499
|
+
expected: 'Bloom downsample params upload succeeds',
|
|
1328
1500
|
hint: 'repair the Bloom params buffer and retry the frame',
|
|
1329
1501
|
});
|
|
1330
1502
|
const errors: unknown[] = [];
|
|
1331
1503
|
const unsubscribe = scenario.renderer.onError((error) => errors.push(error));
|
|
1332
1504
|
try {
|
|
1333
1505
|
expect(scenario.draw().ok).toBe(true);
|
|
1334
|
-
const
|
|
1506
|
+
const downsampleParams = createBuffer.mock.calls.reduce<object | undefined>(
|
|
1335
1507
|
(found, args, index) => {
|
|
1336
|
-
if (found !== undefined || args[0].label !== 'bloom-
|
|
1508
|
+
if (found !== undefined || args[0].label !== 'bloom-downsample-params-ubo') return found;
|
|
1337
1509
|
const result = createBuffer.mock.results[index];
|
|
1338
1510
|
if (result?.type !== 'return' || !result.value.ok) return undefined;
|
|
1339
1511
|
return result.value.value as object;
|
|
1340
1512
|
},
|
|
1341
1513
|
undefined,
|
|
1342
1514
|
);
|
|
1343
|
-
expect(
|
|
1515
|
+
expect(downsampleParams).toBeDefined();
|
|
1344
1516
|
const lkg = scenario.renderer.inspect().bloom;
|
|
1345
|
-
expect(lkg).toMatchObject({ uploadCount:
|
|
1517
|
+
expect(lkg).toMatchObject({ uploadCount: 10, bindGroupCount: 10, encodeCount: 10 });
|
|
1346
1518
|
const submitCount = submit.mock.calls.length;
|
|
1347
1519
|
|
|
1348
1520
|
let injected = false;
|
|
1349
1521
|
writeBuffer.mockImplementation((buffer, ...args) => {
|
|
1350
|
-
if (buffer ===
|
|
1522
|
+
if (buffer === downsampleParams && !injected) {
|
|
1351
1523
|
injected = true;
|
|
1352
1524
|
return err(failure);
|
|
1353
1525
|
}
|
|
@@ -1359,23 +1531,23 @@ describe('factory contract', () => {
|
|
|
1359
1531
|
expect(errors).toHaveLength(1);
|
|
1360
1532
|
expect(errors[0]).toMatchObject({
|
|
1361
1533
|
code: 'pass-encode-failed',
|
|
1362
|
-
detail: { passName: 'bloom-
|
|
1534
|
+
detail: { passName: 'bloom-downsample-0', cause: failure },
|
|
1363
1535
|
});
|
|
1364
1536
|
expect(scenario.renderer.inspect().bloom).toMatchObject({
|
|
1365
1537
|
state: 'active',
|
|
1366
1538
|
generation: lkg.generation,
|
|
1367
|
-
uploadCount:
|
|
1368
|
-
bindGroupCount:
|
|
1369
|
-
encodeCount:
|
|
1539
|
+
uploadCount: 10,
|
|
1540
|
+
bindGroupCount: 10,
|
|
1541
|
+
encodeCount: 10,
|
|
1370
1542
|
});
|
|
1371
1543
|
|
|
1372
1544
|
writeBuffer.mockRestore();
|
|
1373
1545
|
expect(scenario.draw().ok).toBe(true);
|
|
1374
1546
|
expect(submit).toHaveBeenCalledTimes(submitCount + 1);
|
|
1375
1547
|
expect(scenario.renderer.inspect().bloom).toMatchObject({
|
|
1376
|
-
uploadCount:
|
|
1377
|
-
bindGroupCount:
|
|
1378
|
-
encodeCount:
|
|
1548
|
+
uploadCount: 10,
|
|
1549
|
+
bindGroupCount: 10,
|
|
1550
|
+
encodeCount: 10,
|
|
1379
1551
|
});
|
|
1380
1552
|
} finally {
|
|
1381
1553
|
writeBuffer.mockRestore();
|
|
@@ -1388,18 +1560,17 @@ describe('factory contract', () => {
|
|
|
1388
1560
|
|
|
1389
1561
|
const bloomConstructionFailures = [
|
|
1390
1562
|
['createSampler', 'bloom-sampler'],
|
|
1391
|
-
['createBindGroupLayout', 'bloom-
|
|
1392
|
-
['createBindGroupLayout', 'bloom-
|
|
1563
|
+
['createBindGroupLayout', 'bloom-downsample-bgl'],
|
|
1564
|
+
['createBindGroupLayout', 'bloom-upsample-bgl'],
|
|
1393
1565
|
['createBindGroupLayout', 'bloom-composite-bgl'],
|
|
1394
|
-
['createPipelineLayout', 'bloom-
|
|
1395
|
-
['createPipelineLayout', 'bloom-
|
|
1566
|
+
['createPipelineLayout', 'bloom-downsample-pl'],
|
|
1567
|
+
['createPipelineLayout', 'bloom-upsample-pl'],
|
|
1396
1568
|
['createPipelineLayout', 'bloom-composite-pl'],
|
|
1397
|
-
['createRenderPipeline', 'bloom-
|
|
1398
|
-
['createRenderPipeline', 'bloom-
|
|
1569
|
+
['createRenderPipeline', 'bloom-downsample-pipeline'],
|
|
1570
|
+
['createRenderPipeline', 'bloom-upsample-pipeline'],
|
|
1399
1571
|
['createRenderPipeline', 'bloom-composite-pipeline'],
|
|
1400
|
-
['createBuffer', 'bloom-
|
|
1401
|
-
['createBuffer', 'bloom-
|
|
1402
|
-
['createBuffer', 'bloom-blur-v-params-ubo'],
|
|
1572
|
+
['createBuffer', 'bloom-downsample-params-ubo'],
|
|
1573
|
+
['createBuffer', 'bloom-upsample-params-ubo'],
|
|
1403
1574
|
['createBuffer', 'bloom-composite-params-ubo'],
|
|
1404
1575
|
] as const;
|
|
1405
1576
|
|
|
@@ -1464,11 +1635,11 @@ describe('factory contract', () => {
|
|
|
1464
1635
|
expect(scenario.renderer.inspect().bloom).toMatchObject({
|
|
1465
1636
|
state: 'active',
|
|
1466
1637
|
graphStatus: 'valid',
|
|
1467
|
-
targetCount:
|
|
1468
|
-
passCount:
|
|
1469
|
-
uploadCount:
|
|
1470
|
-
bindGroupCount:
|
|
1471
|
-
encodeCount:
|
|
1638
|
+
targetCount: 10,
|
|
1639
|
+
passCount: 10,
|
|
1640
|
+
uploadCount: 10,
|
|
1641
|
+
bindGroupCount: 10,
|
|
1642
|
+
encodeCount: 10,
|
|
1472
1643
|
});
|
|
1473
1644
|
} finally {
|
|
1474
1645
|
operation.mockRestore();
|
|
@@ -1485,7 +1656,7 @@ describe('factory contract', () => {
|
|
|
1485
1656
|
const submit = vi.spyOn(RhiNullQueue.prototype, 'submit');
|
|
1486
1657
|
const failure = new RhiError({
|
|
1487
1658
|
code: 'webgpu-runtime-error',
|
|
1488
|
-
expected: 'Bloom
|
|
1659
|
+
expected: 'Bloom upsample bind-group creation succeeds',
|
|
1489
1660
|
hint: 'repair the Bloom bind group and retry the frame',
|
|
1490
1661
|
});
|
|
1491
1662
|
let injected = false;
|
|
@@ -1496,7 +1667,7 @@ describe('factory contract', () => {
|
|
|
1496
1667
|
scenario.canvas.width = 96;
|
|
1497
1668
|
scenario.canvas.height = 64;
|
|
1498
1669
|
createBindGroup.mockImplementation(function (this: RhiNullDevice, descriptor) {
|
|
1499
|
-
if (!injected && descriptor.label === 'bloom-
|
|
1670
|
+
if (!injected && descriptor.label === 'bloom-upsample-3-bg') {
|
|
1500
1671
|
injected = true;
|
|
1501
1672
|
return err(failure);
|
|
1502
1673
|
}
|
|
@@ -1508,9 +1679,9 @@ describe('factory contract', () => {
|
|
|
1508
1679
|
expect(scenario.renderer.inspect().bloom).toMatchObject({
|
|
1509
1680
|
state: 'active',
|
|
1510
1681
|
generation: lkg.generation,
|
|
1511
|
-
uploadCount:
|
|
1512
|
-
bindGroupCount:
|
|
1513
|
-
encodeCount:
|
|
1682
|
+
uploadCount: 10,
|
|
1683
|
+
bindGroupCount: 10,
|
|
1684
|
+
encodeCount: 10,
|
|
1514
1685
|
});
|
|
1515
1686
|
|
|
1516
1687
|
createBindGroup.mockRestore();
|
|
@@ -1518,9 +1689,9 @@ describe('factory contract', () => {
|
|
|
1518
1689
|
expect(submit).toHaveBeenCalledTimes(submitCount + 1);
|
|
1519
1690
|
expect(scenario.renderer.inspect().bloom).toMatchObject({
|
|
1520
1691
|
state: 'active',
|
|
1521
|
-
uploadCount:
|
|
1522
|
-
bindGroupCount:
|
|
1523
|
-
encodeCount:
|
|
1692
|
+
uploadCount: 10,
|
|
1693
|
+
bindGroupCount: 10,
|
|
1694
|
+
encodeCount: 10,
|
|
1524
1695
|
});
|
|
1525
1696
|
} finally {
|
|
1526
1697
|
createBindGroup.mockRestore();
|
|
@@ -1557,10 +1728,10 @@ describe('factory contract', () => {
|
|
|
1557
1728
|
expect(overlap.state).toBe('active');
|
|
1558
1729
|
expect(overlap.graphStatus).toBe('valid');
|
|
1559
1730
|
expect(overlap.enabled).toBe(true);
|
|
1560
|
-
expect(overlap.passCount).toBe(
|
|
1561
|
-
expect(overlap.uploadCount).toBe(
|
|
1562
|
-
expect(overlap.bindGroupCount).toBe(
|
|
1563
|
-
expect(overlap.encodeCount).toBe(
|
|
1731
|
+
expect(overlap.passCount).toBe(10);
|
|
1732
|
+
expect(overlap.uploadCount).toBe(10);
|
|
1733
|
+
expect(overlap.bindGroupCount).toBe(10);
|
|
1734
|
+
expect(overlap.encodeCount).toBe(10);
|
|
1564
1735
|
expect(overlap.resourceCount).toBeGreaterThan(retiring.resourceCount);
|
|
1565
1736
|
|
|
1566
1737
|
fence.resolve(undefined);
|
|
@@ -1569,9 +1740,9 @@ describe('factory contract', () => {
|
|
|
1569
1740
|
const settled = scenario.renderer.inspect().bloom;
|
|
1570
1741
|
expect(settled.state).toBe('active');
|
|
1571
1742
|
expect(settled.graphStatus).toBe('valid');
|
|
1572
|
-
expect(settled.passCount).toBe(
|
|
1573
|
-
expect(settled.targetCount).toBe(
|
|
1574
|
-
expect(settled.targetBytes).toBe(
|
|
1743
|
+
expect(settled.passCount).toBe(10);
|
|
1744
|
+
expect(settled.targetCount).toBe(10);
|
|
1745
|
+
expect(settled.targetBytes).toBe(54_560);
|
|
1575
1746
|
expect(settled.resourceCount).toBeLessThan(overlap.resourceCount);
|
|
1576
1747
|
} finally {
|
|
1577
1748
|
onSubmittedWorkDone.mockRestore();
|
|
@@ -1797,7 +1968,7 @@ describe('factory contract', () => {
|
|
|
1797
1968
|
const destroyCounts = new Map<object, number>();
|
|
1798
1969
|
const failure = new RhiError({
|
|
1799
1970
|
code: 'webgpu-runtime-error',
|
|
1800
|
-
expected: 'Bloom
|
|
1971
|
+
expected: 'Bloom upsample params buffer construction succeeds',
|
|
1801
1972
|
hint: 'repair the injected construction failure and retry the same frame',
|
|
1802
1973
|
});
|
|
1803
1974
|
try {
|
|
@@ -1807,7 +1978,7 @@ describe('factory contract', () => {
|
|
|
1807
1978
|
const lkg = scenario.renderer.inspect().bloom;
|
|
1808
1979
|
scenario.setBloom('on');
|
|
1809
1980
|
createBuffer.mockImplementation(function (this: RhiNullDevice, descriptor) {
|
|
1810
|
-
if (descriptor.label === 'bloom-
|
|
1981
|
+
if (descriptor.label === 'bloom-upsample-params-ubo') {
|
|
1811
1982
|
return err(failure);
|
|
1812
1983
|
}
|
|
1813
1984
|
const result = originalCreateBuffer.call(this, descriptor);
|
|
@@ -1832,12 +2003,12 @@ describe('factory contract', () => {
|
|
|
1832
2003
|
expect(scenario.renderer.inspect().bloom.resourceCount).toBe(lkg.resourceCount);
|
|
1833
2004
|
expect(scenario.renderer.inspect().bloom.passCount).toBe(0);
|
|
1834
2005
|
const adoptedBeforeFailure = [...created.entries()].filter(
|
|
1835
|
-
([, label]) => label === 'bloom-
|
|
2006
|
+
([, label]) => label === 'bloom-downsample-params-ubo',
|
|
1836
2007
|
);
|
|
1837
|
-
expect(adoptedBeforeFailure).toHaveLength(
|
|
2008
|
+
expect(adoptedBeforeFailure).toHaveLength(1);
|
|
1838
2009
|
expect(adoptedBeforeFailure.every(([handle]) => destroyCounts.get(handle) === 1)).toBe(true);
|
|
1839
2010
|
expect(
|
|
1840
|
-
[...created.values()].filter((label) => label === 'bloom-
|
|
2011
|
+
[...created.values()].filter((label) => label === 'bloom-upsample-params-ubo'),
|
|
1841
2012
|
).toHaveLength(0);
|
|
1842
2013
|
createBuffer.mockRestore();
|
|
1843
2014
|
destroyBuffer.mockRestore();
|
|
@@ -1902,6 +2073,7 @@ describe('factory contract', () => {
|
|
|
1902
2073
|
near: 0.1,
|
|
1903
2074
|
far: 100,
|
|
1904
2075
|
antialias: ANTIALIAS_NONE,
|
|
2076
|
+
tonemap: TONEMAP_ACES_FILMIC,
|
|
1905
2077
|
bloom: BLOOM_ENABLED,
|
|
1906
2078
|
},
|
|
1907
2079
|
},
|
|
@@ -1977,6 +2149,96 @@ describe('factory contract', () => {
|
|
|
1977
2149
|
}
|
|
1978
2150
|
});
|
|
1979
2151
|
|
|
2152
|
+
it('does not publish Bloom resources for an intensity-zero recovery candidate', async () => {
|
|
2153
|
+
const adapter = new RhiNullAdapter();
|
|
2154
|
+
const lost = deferred<{ readonly reason: 'unknown'; readonly message: string }>();
|
|
2155
|
+
const firstDevice = (await adapter.requestDevice()).unwrap();
|
|
2156
|
+
const replacementDevice = (await adapter.requestDevice()).unwrap();
|
|
2157
|
+
const lostProxy = new Proxy(firstDevice, {
|
|
2158
|
+
get(target, property) {
|
|
2159
|
+
if (property === 'lost') return lost.promise;
|
|
2160
|
+
const value = Reflect.get(target, property, target);
|
|
2161
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
2162
|
+
},
|
|
2163
|
+
});
|
|
2164
|
+
let requestCount = 0;
|
|
2165
|
+
const recoverableRhi = {
|
|
2166
|
+
...rhi,
|
|
2167
|
+
requestAdapter: async () =>
|
|
2168
|
+
ok({
|
|
2169
|
+
features: adapter.features,
|
|
2170
|
+
limits: adapter.limits,
|
|
2171
|
+
requestDevice: async () => {
|
|
2172
|
+
requestCount += 1;
|
|
2173
|
+
return requestCount === 1 ? ok(lostProxy) : ok(replacementDevice);
|
|
2174
|
+
},
|
|
2175
|
+
} as never),
|
|
2176
|
+
} as never;
|
|
2177
|
+
const renderer = await constructRenderer(
|
|
2178
|
+
{ width: 64, height: 64, getContext: () => null },
|
|
2179
|
+
{ rhi: recoverableRhi },
|
|
2180
|
+
{ shaderManifestUrl: renderLifecycleManifestUrl() },
|
|
2181
|
+
);
|
|
2182
|
+
const world = new World();
|
|
2183
|
+
const attached = renderer.attach(world);
|
|
2184
|
+
expect(attached.ok).toBe(true);
|
|
2185
|
+
if (!attached.ok) return;
|
|
2186
|
+
registerPropagateTransforms(world);
|
|
2187
|
+
world.spawn(
|
|
2188
|
+
{ component: Transform, data: { pos: [0, 0, 4] } },
|
|
2189
|
+
{
|
|
2190
|
+
component: LocalCamera,
|
|
2191
|
+
data: {
|
|
2192
|
+
fov: 1,
|
|
2193
|
+
aspect: 1,
|
|
2194
|
+
near: 0.1,
|
|
2195
|
+
far: 100,
|
|
2196
|
+
antialias: ANTIALIAS_NONE,
|
|
2197
|
+
tonemap: TONEMAP_ACES_FILMIC,
|
|
2198
|
+
bloom: BLOOM_ENABLED,
|
|
2199
|
+
bloomIntensity: 0,
|
|
2200
|
+
},
|
|
2201
|
+
},
|
|
2202
|
+
);
|
|
2203
|
+
const input = () => ({
|
|
2204
|
+
leases: [attached.value],
|
|
2205
|
+
camera: { lease: attached.value },
|
|
2206
|
+
environment: { lease: attached.value },
|
|
2207
|
+
});
|
|
2208
|
+
const assertZero = (): void => {
|
|
2209
|
+
expect(renderer.inspect().bloom).toMatchObject({
|
|
2210
|
+
graphStatus: 'empty',
|
|
2211
|
+
enabled: false,
|
|
2212
|
+
targetCount: 0,
|
|
2213
|
+
targetBytes: 0,
|
|
2214
|
+
resourceCount: 0,
|
|
2215
|
+
passCount: 0,
|
|
2216
|
+
encodeCount: 0,
|
|
2217
|
+
bindGroupCount: 0,
|
|
2218
|
+
uploadCount: 0,
|
|
2219
|
+
residentChildBytes: 0,
|
|
2220
|
+
state: 'off',
|
|
2221
|
+
});
|
|
2222
|
+
};
|
|
2223
|
+
try {
|
|
2224
|
+
expect((await renderer.initialization).ok).toBe(true);
|
|
2225
|
+
expect(world.update().ok).toBe(true);
|
|
2226
|
+
expect(renderer.draw(input()).ok).toBe(true);
|
|
2227
|
+
assertZero();
|
|
2228
|
+
lost.reject({ reason: 'unknown', message: 'forced intensity-zero recovery loss' });
|
|
2229
|
+
await Promise.resolve();
|
|
2230
|
+
await Promise.resolve();
|
|
2231
|
+
expect(renderer.health().reason).toBe('device-lost');
|
|
2232
|
+
expect((await renderer.recover()).ok).toBe(true);
|
|
2233
|
+
assertZero();
|
|
2234
|
+
expect(world.update().ok).toBe(true);
|
|
2235
|
+
expect(renderer.draw(input()).ok).toBe(true);
|
|
2236
|
+
assertZero();
|
|
2237
|
+
} finally {
|
|
2238
|
+
await renderer.dispose();
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
|
|
1980
2242
|
it('abandons a post-process recovery candidate before promotion and retries once', async () => {
|
|
1981
2243
|
const adapter = new RhiNullAdapter();
|
|
1982
2244
|
const lost = deferred<{ readonly reason: 'unknown'; readonly message: string }>();
|
|
@@ -2092,6 +2354,7 @@ describe('factory contract', () => {
|
|
|
2092
2354
|
near: 0.1,
|
|
2093
2355
|
far: 100,
|
|
2094
2356
|
antialias: ANTIALIAS_NONE,
|
|
2357
|
+
tonemap: TONEMAP_ACES_FILMIC,
|
|
2095
2358
|
bloom: BLOOM_DISABLED,
|
|
2096
2359
|
},
|
|
2097
2360
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { Transform } from '@forgeax/engine-scene';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { Atmosphere, Camera, DirectionalLight } from '../components';
|
|
5
5
|
import {
|
|
6
6
|
extractFrame,
|
|
@@ -89,6 +89,44 @@ describe('FrameCamera entity selection', () => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
describe('Atmosphere sun extraction', () => {
|
|
92
|
+
it('routes a missing sun through World, then retries after ownership is fixed', () => {
|
|
93
|
+
const world = new World();
|
|
94
|
+
world.spawn({ component: Atmosphere, data: {} }).unwrap();
|
|
95
|
+
spawnCamera(world, 1);
|
|
96
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
97
|
+
try {
|
|
98
|
+
expect(() =>
|
|
99
|
+
extractFrame(world, prepareExtractContext(world, { renderables: 'none' })),
|
|
100
|
+
).toThrow(
|
|
101
|
+
expect.objectContaining({ code: 'sun-cardinality', detail: { field: 'sun', value: 0 } }),
|
|
102
|
+
);
|
|
103
|
+
const firstRouted = consoleError.mock.calls[0]?.[1] as
|
|
104
|
+
| { code?: string; detail?: { value?: number } }
|
|
105
|
+
| undefined;
|
|
106
|
+
expect(consoleError.mock.calls[0]?.[0]).toBe(
|
|
107
|
+
'[RenderSystem.extract (environment-selection)]',
|
|
108
|
+
);
|
|
109
|
+
expect(firstRouted?.code).toBe('sun-cardinality');
|
|
110
|
+
expect(firstRouted?.detail?.value).toBe(0);
|
|
111
|
+
|
|
112
|
+
// This is the direct extraction boundary. App.onError owns frame-loop
|
|
113
|
+
// Result failures and renderer events; World routes this internal
|
|
114
|
+
// producer error to its documented console boundary.
|
|
115
|
+
|
|
116
|
+
const firstSun = world
|
|
117
|
+
.spawn({
|
|
118
|
+
component: DirectionalLight,
|
|
119
|
+
data: { direction: [0, -0.25, 1], color: [1, 1, 1], intensity: 1 },
|
|
120
|
+
})
|
|
121
|
+
.unwrap();
|
|
122
|
+
const recovered = extractFrame(world, prepareExtractContext(world, { renderables: 'none' }));
|
|
123
|
+
expect(recovered.lights.directional?.entity).toBe(firstSun);
|
|
124
|
+
expect(consoleError).toHaveBeenCalledTimes(1);
|
|
125
|
+
} finally {
|
|
126
|
+
consoleError.mockRestore();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
92
130
|
it('keeps DirectionalLight intensity out of SunCandidate color', () => {
|
|
93
131
|
const world = new World();
|
|
94
132
|
world.spawn({ component: Atmosphere, data: {} }).unwrap();
|
|
@@ -13,6 +13,8 @@ const atmosphereParameters: AtmosphereParameters = {
|
|
|
13
13
|
mieCoefficient: 0.005,
|
|
14
14
|
mieDirectionalG: 0.8,
|
|
15
15
|
sunAngularRadius: 0.004675,
|
|
16
|
+
circumsolarStrength: 1,
|
|
17
|
+
circumsolarWidth: 1,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
function unwrapFrame(result: ReturnType<typeof selectEnvironmentFrame>): EnvironmentFrame {
|