@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
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
FrameObservationRequest,
|
|
3
3
|
FrameReceipt,
|
|
4
4
|
RenderError as PublicRenderError,
|
|
5
|
+
SunCardinalityError as PublicSunCardinalityError,
|
|
5
6
|
ReflectionProbeInspection,
|
|
6
7
|
ReflectionProbeSelectionInspection,
|
|
7
8
|
RendererState,
|
|
@@ -65,6 +66,13 @@ const ssrInspection: SsrSpatialInspection = inspection.ssr;
|
|
|
65
66
|
void ssrInspection;
|
|
66
67
|
type IntentError = Extract<PublicRenderError, { readonly code: 'render-intent-invalid' }>;
|
|
67
68
|
expectTypeOf<IntentError['detail']>().toEqualTypeOf<RenderIntentInvalidDetail>();
|
|
69
|
+
type PublicSunCardinalityCode = Extract<PublicRenderError['code'], 'sun-cardinality'>;
|
|
70
|
+
expectTypeOf<PublicSunCardinalityCode>().toEqualTypeOf<'sun-cardinality'>();
|
|
71
|
+
expectTypeOf<PublicSunCardinalityError['code']>().toEqualTypeOf<'sun-cardinality'>();
|
|
72
|
+
expectTypeOf<PublicSunCardinalityError['detail']>().toEqualTypeOf<{
|
|
73
|
+
readonly field: 'sun';
|
|
74
|
+
readonly value: number;
|
|
75
|
+
}>();
|
|
68
76
|
const backendKind: RenderInspection['capabilities']['backendKind'] =
|
|
69
77
|
inspection.capabilities.backendKind;
|
|
70
78
|
void backendKind;
|
|
@@ -39,6 +39,7 @@ const expectedCodes = [
|
|
|
39
39
|
'dynamic-resolution-invalid-parameter',
|
|
40
40
|
'dynamic-resolution-requires-taa',
|
|
41
41
|
'dynamic-resolution-timing-unavailable',
|
|
42
|
+
'motion-blur-invalid-params',
|
|
42
43
|
'shadow-invalid-config',
|
|
43
44
|
'equirect-projection-failed',
|
|
44
45
|
'standard-light-budget-exceeded',
|
|
@@ -63,6 +64,7 @@ const expectedCodes = [
|
|
|
63
64
|
'render-feature-draw-recording-failed',
|
|
64
65
|
'environment-source-conflict',
|
|
65
66
|
'fog-cardinality',
|
|
67
|
+
'sun-cardinality',
|
|
66
68
|
'taa-caps-insufficient',
|
|
67
69
|
'environment-generation-failed',
|
|
68
70
|
'atmosphere-invalid-parameter',
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES,
|
|
4
|
+
shouldReportPendingRenderFeatureError,
|
|
5
|
+
} from '../features/error-reporting.js';
|
|
6
|
+
|
|
7
|
+
describe('pending render-feature error reporting', () => {
|
|
8
|
+
it('reports the first failure, suppresses the hot-loop, and reopens the cooldown', () => {
|
|
9
|
+
const lastReportedFrame = new Map<string, number>();
|
|
10
|
+
expect(shouldReportPendingRenderFeatureError(lastReportedFrame, 'feature\0plan', 10)).toBe(
|
|
11
|
+
true,
|
|
12
|
+
);
|
|
13
|
+
expect(shouldReportPendingRenderFeatureError(lastReportedFrame, 'feature\0plan', 11)).toBe(
|
|
14
|
+
false,
|
|
15
|
+
);
|
|
16
|
+
expect(
|
|
17
|
+
shouldReportPendingRenderFeatureError(
|
|
18
|
+
lastReportedFrame,
|
|
19
|
+
'feature\0plan',
|
|
20
|
+
10 + PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES,
|
|
21
|
+
),
|
|
22
|
+
).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -89,6 +89,29 @@ function frame(host: RenderFeatureHost, frameNumber: number) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
describe('RenderFeature stage fault recovery lifecycle', () => {
|
|
92
|
+
it('retains the original exception when a feature stage throws', () => {
|
|
93
|
+
const cause = new TypeError('feature plan exploded');
|
|
94
|
+
const host = createRenderFeatureHost(
|
|
95
|
+
[
|
|
96
|
+
{
|
|
97
|
+
identity: 'synthetic.m10.throwing',
|
|
98
|
+
extract: () => ok({ ready: true }),
|
|
99
|
+
plan: () => {
|
|
100
|
+
throw cause;
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
caps,
|
|
105
|
+
).unwrap();
|
|
106
|
+
|
|
107
|
+
const result = frame(host, 1);
|
|
108
|
+
expect(result.errors[0]).toMatchObject({
|
|
109
|
+
code: 'render-feature-stage-failed',
|
|
110
|
+
detail: { featureIdentity: 'synthetic.m10.throwing', cause },
|
|
111
|
+
});
|
|
112
|
+
expect(host.diagnostics()[0]?.latestError).toMatchObject({ detail: { cause } });
|
|
113
|
+
});
|
|
114
|
+
|
|
92
115
|
it.each<Fault>([
|
|
93
116
|
'create',
|
|
94
117
|
'prepared-resource',
|
|
@@ -130,4 +130,35 @@ describe('RenderScene temporal retry boundary', () => {
|
|
|
130
130
|
instanceCollections.dispose();
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
it('keeps instance motion valid when stable generations reorder', () => {
|
|
135
|
+
const instanceCollections = new InstanceCollectionStore();
|
|
136
|
+
const collection = instanceCollections.create({ transforms: matrices(2, 1) }).unwrap();
|
|
137
|
+
const initialInstances = instanceCollections.snapshot(collection.collectionId).unwrap();
|
|
138
|
+
instanceCollections
|
|
139
|
+
.replace(
|
|
140
|
+
collection.collectionId,
|
|
141
|
+
matrices(2, 9),
|
|
142
|
+
new Uint32Array([
|
|
143
|
+
initialInstances.generations[1] ?? 0,
|
|
144
|
+
initialInstances.generations[0] ?? 0,
|
|
145
|
+
]),
|
|
146
|
+
)
|
|
147
|
+
.unwrap();
|
|
148
|
+
const reorderedInstances = instanceCollections.snapshot(collection.collectionId).unwrap();
|
|
149
|
+
const scene = new RenderScene();
|
|
150
|
+
scene.setTemporalTracking(true);
|
|
151
|
+
try {
|
|
152
|
+
scene.apply([updateSnapshot(snapshot(7, 1, initialInstances))]);
|
|
153
|
+
submit(scene, 7);
|
|
154
|
+
scene.apply([updateInstances(snapshot(7, 1, reorderedInstances))]);
|
|
155
|
+
|
|
156
|
+
const temporal = scene.temporalSnapshotBySlot(0);
|
|
157
|
+
expect(temporal?.motionValid).toBe(true);
|
|
158
|
+
expect(temporal?.previousInstances?.generations).toEqual(initialInstances.generations);
|
|
159
|
+
expect(temporal?.previousInstances?.transforms).toEqual(initialInstances.transforms);
|
|
160
|
+
} finally {
|
|
161
|
+
instanceCollections.dispose();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
133
164
|
});
|
|
@@ -87,7 +87,8 @@ function temporalResolveFrame(
|
|
|
87
87
|
bloom: 'off',
|
|
88
88
|
bloomThreshold: 1,
|
|
89
89
|
bloomIntensity: 1,
|
|
90
|
-
|
|
90
|
+
bloomSoftKnee: 0.5,
|
|
91
|
+
bloomScatter: 0.7,
|
|
91
92
|
clearColor: [0, 0, 0, 1],
|
|
92
93
|
temporal: frameState.lastSuccessfulTemporalView,
|
|
93
94
|
},
|
|
@@ -1147,6 +1148,7 @@ describe('renderer successful-submit transaction', () => {
|
|
|
1147
1148
|
height: 4,
|
|
1148
1149
|
frameIndex: 6,
|
|
1149
1150
|
historyValid: true,
|
|
1151
|
+
sampleTimeSeconds: 1,
|
|
1150
1152
|
});
|
|
1151
1153
|
const frameState = {
|
|
1152
1154
|
temporalGpuState: undefined,
|
|
@@ -1160,6 +1162,7 @@ describe('renderer successful-submit transaction', () => {
|
|
|
1160
1162
|
height: 4,
|
|
1161
1163
|
frameIndex: 6,
|
|
1162
1164
|
historyValid: true,
|
|
1165
|
+
sampleTimeSeconds: 1.016,
|
|
1163
1166
|
}),
|
|
1164
1167
|
},
|
|
1165
1168
|
environmentGeneration,
|
|
@@ -1244,6 +1247,7 @@ describe('renderer successful-submit transaction', () => {
|
|
|
1244
1247
|
expect(active.valid).toBe(true);
|
|
1245
1248
|
expect(active.readIndex).toBe(1);
|
|
1246
1249
|
expect(frameState.lastSuccessfulTemporalView).toBe(lastSuccessfulTemporalView);
|
|
1250
|
+
expect(frameState.lastSuccessfulTemporalView.sampleTimeSeconds).toBe(1);
|
|
1247
1251
|
expect(environmentLifecycle.inspect().activeSignature).toBe(activeEnvironment.signature);
|
|
1248
1252
|
expect(environmentLifecycle.inspect().lkgSignature).toBe(activeEnvironment.signature);
|
|
1249
1253
|
frameState.environmentGeneration = environmentLifecycle
|
|
@@ -1257,6 +1261,7 @@ describe('renderer successful-submit transaction', () => {
|
|
|
1257
1261
|
height: 8,
|
|
1258
1262
|
frameIndex: 7,
|
|
1259
1263
|
historyValid: true,
|
|
1264
|
+
sampleTimeSeconds: 1.016,
|
|
1260
1265
|
}),
|
|
1261
1266
|
};
|
|
1262
1267
|
const retryFrame = temporalResolveFrame(device, scope, frameState, {});
|
|
@@ -1273,6 +1278,7 @@ describe('renderer successful-submit transaction', () => {
|
|
|
1273
1278
|
expect(gpuCommitAfterSubmit).toEqual([1]);
|
|
1274
1279
|
expect(frameState.activeTemporalGpuState).not.toBe(active);
|
|
1275
1280
|
expect(frameState.successfulTemporalFrameIndex).toBe(8);
|
|
1281
|
+
expect(frameState.lastSuccessfulTemporalView?.sampleTimeSeconds).toBe(1.016);
|
|
1276
1282
|
expect(frameState.temporalGpuState).toBeUndefined();
|
|
1277
1283
|
expect(frameState.retiringTemporalGpuStates.size).toBe(1);
|
|
1278
1284
|
expect(publishEnvironment).toHaveBeenCalledTimes(1);
|
|
@@ -43,14 +43,14 @@ const entries: readonly FixtureEntry[] = [
|
|
|
43
43
|
bindings: '[]',
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
|
-
hash: 'fixture-bloom-
|
|
47
|
-
wgsl: '// fixture marker:
|
|
46
|
+
hash: 'fixture-bloom-downsample',
|
|
47
|
+
wgsl: '// fixture marker: BloomDownsampleParams\n',
|
|
48
48
|
glsl: '',
|
|
49
49
|
bindings: '[]',
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
|
-
hash: 'fixture-bloom-
|
|
53
|
-
wgsl: '// fixture marker:
|
|
52
|
+
hash: 'fixture-bloom-upsample',
|
|
53
|
+
wgsl: '// fixture marker: BloomUpsampleParams\n',
|
|
54
54
|
glsl: '',
|
|
55
55
|
bindings: '[]',
|
|
56
56
|
},
|
|
@@ -60,6 +60,8 @@ describe('skinned shadow caster', () => {
|
|
|
60
60
|
expect(shader).toContain('discard;');
|
|
61
61
|
expect(shader).toContain('@group(2) @binding(1)');
|
|
62
62
|
expect(shader).toContain('let skinMatrix = palette[');
|
|
63
|
+
expect(shader).toContain('materialIndex = u32(meshes[meshIndex].temporal.w);');
|
|
64
|
+
expect(shader).not.toContain('bitcast<u32>(meshes[meshIndex].temporal.w)');
|
|
63
65
|
expect(shader).toContain('@location(13) color : vec4<f32>');
|
|
64
66
|
expect(shader).toContain('out.color = in.color;');
|
|
65
67
|
expect(record).toContain('entry.source.skin.byteOffset');
|
|
@@ -31,7 +31,7 @@ function topology(): RenderPipelineTopology {
|
|
|
31
31
|
storageFormat: 'bgra8unorm',
|
|
32
32
|
viewFormat: 'bgra8unorm-srgb',
|
|
33
33
|
},
|
|
34
|
-
camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on' },
|
|
34
|
+
camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom: 'on', bloomIntensity: 1 },
|
|
35
35
|
shadow: {
|
|
36
36
|
directional: { mapSize: 64, cascadeCount: 1 },
|
|
37
37
|
spotMapSize: 64,
|
|
@@ -35,10 +35,15 @@ const dualViewProfile: SurfaceProfile = {
|
|
|
35
35
|
hasDisplayEndpoint: true,
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
type TopologyOverrides = Omit<Partial<RenderPipelineTopology>, 'camera'> & {
|
|
39
|
+
readonly camera?: Partial<RenderPipelineTopology['camera']>;
|
|
40
|
+
};
|
|
41
|
+
|
|
38
42
|
function topology(
|
|
39
43
|
profile: StandardProfile,
|
|
40
|
-
overrides:
|
|
44
|
+
overrides: TopologyOverrides = {},
|
|
41
45
|
): RenderPipelineTopology {
|
|
46
|
+
const { camera: cameraOverride, ...rest } = overrides;
|
|
42
47
|
return {
|
|
43
48
|
pipelineId: STANDARD_PIPELINE_ID,
|
|
44
49
|
standardProfile: profile,
|
|
@@ -49,7 +54,13 @@ function topology(
|
|
|
49
54
|
storageFormat: 'bgra8unorm',
|
|
50
55
|
viewFormat: 'bgra8unorm-srgb',
|
|
51
56
|
},
|
|
52
|
-
camera: {
|
|
57
|
+
camera: {
|
|
58
|
+
tonemap: 'aces-filmic',
|
|
59
|
+
antialias: 'fxaa',
|
|
60
|
+
bloom: 'on',
|
|
61
|
+
bloomIntensity: 1,
|
|
62
|
+
...cameraOverride,
|
|
63
|
+
},
|
|
53
64
|
shadow: {
|
|
54
65
|
directional: { mapSize: 64, cascadeCount: 1 },
|
|
55
66
|
spotMapSize: 64,
|
|
@@ -65,7 +76,7 @@ function topology(
|
|
|
65
76
|
},
|
|
66
77
|
featureTopologySignature: 'none',
|
|
67
78
|
gpuDrivenTopologySignature: '',
|
|
68
|
-
...
|
|
79
|
+
...rest,
|
|
69
80
|
};
|
|
70
81
|
}
|
|
71
82
|
|
|
@@ -109,7 +120,7 @@ function standardLightingFor(resolvedTopology: RenderPipelineTopology) {
|
|
|
109
120
|
return standardLighting;
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
async function build(profile: StandardProfile, overrides?:
|
|
123
|
+
async function build(profile: StandardProfile, overrides?: TopologyOverrides) {
|
|
113
124
|
const graph = new RenderGraphBuilder<RenderPipelineFrame>();
|
|
114
125
|
const resolvedTopology = topology(profile, overrides);
|
|
115
126
|
const histogram = graph.createBuffer('standard-auto-exposure-histogram', { size: 1024 });
|
|
@@ -232,7 +243,7 @@ function importedTaaHistory(graph: RenderGraphBuilder<RenderPipelineFrame>) {
|
|
|
232
243
|
};
|
|
233
244
|
}
|
|
234
245
|
|
|
235
|
-
async function buildTaa(profile: StandardProfile, overrides?:
|
|
246
|
+
async function buildTaa(profile: StandardProfile, overrides?: TopologyOverrides) {
|
|
236
247
|
const graph = new RenderGraphBuilder<RenderPipelineFrame>();
|
|
237
248
|
const history = importedTaaHistory(graph);
|
|
238
249
|
const resolvedTopology = topology(profile, overrides);
|
|
@@ -542,9 +553,8 @@ describe('forgeax::standard graph', () => {
|
|
|
542
553
|
expect(result.ok).toBe(true);
|
|
543
554
|
if (!result.ok) return;
|
|
544
555
|
const names = result.value.inspect().passes.map((pass) => pass.name);
|
|
545
|
-
expect(names).
|
|
546
|
-
expect(names).
|
|
547
|
-
expect(names).not.toContain('bloom-blur-v');
|
|
556
|
+
expect(names.some((name) => name.startsWith('bloom-downsample-'))).toBe(false);
|
|
557
|
+
expect(names.some((name) => name.startsWith('bloom-upsample-'))).toBe(false);
|
|
548
558
|
expect(names).not.toContain('bloom-composite');
|
|
549
559
|
expect(names).toContain('taa-resolve');
|
|
550
560
|
expect(names).toContain('output-transform');
|
|
@@ -37,6 +37,16 @@ describe('TemporalView reset matrix', () => {
|
|
|
37
37
|
expect(resolveTemporalReset(previous, next)).toBeUndefined();
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
it('applies camera reset reasons to motion-blur-only off views', () => {
|
|
41
|
+
const previous = createTemporalView({ ...base().input, antialias: 'none' });
|
|
42
|
+
const next = createTemporalView({
|
|
43
|
+
...previous.input,
|
|
44
|
+
antialias: 'none',
|
|
45
|
+
viewIdentity: 'secondary',
|
|
46
|
+
});
|
|
47
|
+
expect(resolveTemporalReset(previous, next)).toBe('view-switch');
|
|
48
|
+
});
|
|
49
|
+
|
|
40
50
|
it('treats an explicit off commit as a first-frame boundary when TAA resumes', () => {
|
|
41
51
|
const previous = base();
|
|
42
52
|
const off = createTemporalView({
|
|
@@ -130,7 +130,8 @@ describe('TemporalView v1', () => {
|
|
|
130
130
|
bloom: 'disabled',
|
|
131
131
|
bloomThreshold: 1,
|
|
132
132
|
bloomIntensity: 0,
|
|
133
|
-
|
|
133
|
+
bloomSoftKnee: 0.5,
|
|
134
|
+
bloomScatter: 0.7,
|
|
134
135
|
clearColor: [0, 0, 0, 1] as const,
|
|
135
136
|
} as unknown as CameraSnapshot;
|
|
136
137
|
const light = {
|
|
@@ -32,6 +32,10 @@ import { standardTemporalPostOrder } from '../temporal/standard-scene-data';
|
|
|
32
32
|
|
|
33
33
|
let device: RhiDevice;
|
|
34
34
|
|
|
35
|
+
type TopologyOverrides = Omit<Partial<RenderPipelineTopology>, 'camera'> & {
|
|
36
|
+
readonly camera?: Partial<RenderPipelineTopology['camera']>;
|
|
37
|
+
};
|
|
38
|
+
|
|
35
39
|
beforeAll(async () => {
|
|
36
40
|
const adapter = await rhi.requestAdapter();
|
|
37
41
|
if (!adapter.ok) throw adapter.error;
|
|
@@ -42,8 +46,9 @@ beforeAll(async () => {
|
|
|
42
46
|
|
|
43
47
|
function topology(
|
|
44
48
|
pipelineId: 'forgeax::standard',
|
|
45
|
-
overrides:
|
|
49
|
+
overrides: TopologyOverrides = {},
|
|
46
50
|
): RenderPipelineTopology {
|
|
51
|
+
const { camera: cameraOverride, ...rest } = overrides;
|
|
47
52
|
return {
|
|
48
53
|
pipelineId,
|
|
49
54
|
config: undefined,
|
|
@@ -53,7 +58,13 @@ function topology(
|
|
|
53
58
|
storageFormat: 'bgra8unorm',
|
|
54
59
|
viewFormat: 'bgra8unorm-srgb',
|
|
55
60
|
},
|
|
56
|
-
camera: {
|
|
61
|
+
camera: {
|
|
62
|
+
tonemap: 'aces-filmic',
|
|
63
|
+
antialias: 'fxaa',
|
|
64
|
+
bloom: 'on',
|
|
65
|
+
bloomIntensity: 1,
|
|
66
|
+
...cameraOverride,
|
|
67
|
+
},
|
|
57
68
|
shadow: {
|
|
58
69
|
directional: { mapSize: 1024, cascadeCount: 4 },
|
|
59
70
|
spotMapSize: 1024,
|
|
@@ -69,7 +80,7 @@ function topology(
|
|
|
69
80
|
},
|
|
70
81
|
featureTopologySignature: 'none',
|
|
71
82
|
gpuDrivenTopologySignature: '',
|
|
72
|
-
...
|
|
83
|
+
...rest,
|
|
73
84
|
};
|
|
74
85
|
}
|
|
75
86
|
|
|
@@ -877,7 +888,12 @@ describe('typed built-in pipeline topology', () => {
|
|
|
877
888
|
|
|
878
889
|
it('keeps zero-shutter Motion Blur free of temporal target and passes', () => {
|
|
879
890
|
expect(
|
|
880
|
-
isMotionBlurTemporalDemand({
|
|
891
|
+
isMotionBlurTemporalDemand({
|
|
892
|
+
shutterAngle: 0,
|
|
893
|
+
maxRadiusPixels: 32,
|
|
894
|
+
sampleCount: 8,
|
|
895
|
+
targetFps: 60,
|
|
896
|
+
}),
|
|
881
897
|
).toBe(false);
|
|
882
898
|
const compiled = compile(
|
|
883
899
|
standardPipeline,
|
|
@@ -42,7 +42,9 @@ describe('volumetric optics CPU oracle', () => {
|
|
|
42
42
|
|
|
43
43
|
it('converts the shared integrated-light basis once before normalized HG', () => {
|
|
44
44
|
expect(integrateShader).toContain('const VOLUME_LIGHT_PHASE_SCALE : f32 = FOUR_PI;');
|
|
45
|
-
expect(integrateShader
|
|
45
|
+
expect(integrateShader.match(/phase\s*\*\s*VOLUME_LIGHT_PHASE_SCALE\s*\*/g) ?? []).toHaveLength(
|
|
46
|
+
1,
|
|
47
|
+
);
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
it('keeps empty density at identity transmittance', () => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
|
-
import { World } from '@forgeax/engine-ecs';
|
|
4
|
+
import { Time, World } from '@forgeax/engine-ecs';
|
|
5
5
|
import { RenderGraphBuilder } from '@forgeax/engine-render-graph';
|
|
6
6
|
import type {
|
|
7
7
|
BindGroupLayoutDescriptor,
|
|
8
|
+
Buffer,
|
|
8
9
|
RenderPipelineDescriptor,
|
|
9
10
|
RhiAdapter,
|
|
10
11
|
RhiDevice,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
deriveVolumetricFogExtent,
|
|
34
35
|
deriveVolumetricFogResolvedExtent,
|
|
35
36
|
inspectVolumetricFogResources,
|
|
37
|
+
VOLUMETRIC_FOG_PARAMS_BYTES,
|
|
36
38
|
} from '../volume/resources';
|
|
37
39
|
import { resolveVolumeTemporalReset } from '../volume/temporal';
|
|
38
40
|
|
|
@@ -62,15 +64,12 @@ class RecoverableNullDevice extends RhiNullDevice {
|
|
|
62
64
|
private readonly resolveLost: (info: LostInfo) => void;
|
|
63
65
|
private readonly lostPromise: Promise<LostInfo>;
|
|
64
66
|
|
|
65
|
-
constructor() {
|
|
67
|
+
constructor(queue: RhiNullQueue = new RhiNullQueue()) {
|
|
66
68
|
let resolveLost!: (info: LostInfo) => void;
|
|
67
69
|
const lostPromise = new Promise<LostInfo>((resolve) => {
|
|
68
70
|
resolveLost = resolve;
|
|
69
71
|
});
|
|
70
|
-
super(
|
|
71
|
-
new RhiNullQueue(),
|
|
72
|
-
(bookkeeper, device) => new RhiNullCommandEncoder(bookkeeper, device),
|
|
73
|
-
);
|
|
72
|
+
super(queue, (bookkeeper, device) => new RhiNullCommandEncoder(bookkeeper, device));
|
|
74
73
|
this.resolveLost = resolveLost;
|
|
75
74
|
this.lostPromise = lostPromise;
|
|
76
75
|
}
|
|
@@ -104,13 +103,44 @@ class RecoverableNullAdapter implements RhiAdapter {
|
|
|
104
103
|
readonly limits: Readonly<Record<string, number>> = {};
|
|
105
104
|
readonly devices: RecoverableNullDevice[] = [];
|
|
106
105
|
|
|
106
|
+
constructor(private readonly queueFactory: () => RhiNullQueue = () => new RhiNullQueue()) {}
|
|
107
|
+
|
|
107
108
|
requestDevice(): Promise<Result<RhiDevice, RhiError>> {
|
|
108
|
-
const device = new RecoverableNullDevice();
|
|
109
|
+
const device = new RecoverableNullDevice(this.queueFactory());
|
|
109
110
|
this.devices.push(device);
|
|
110
111
|
return Promise.resolve(ok(device));
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
interface CapturedQueueWrite {
|
|
116
|
+
readonly buffer: Buffer;
|
|
117
|
+
readonly offset: number;
|
|
118
|
+
readonly bytes: Uint8Array;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Captures the real record-stage volume parameter writes without adding a
|
|
122
|
+
* second render implementation. */
|
|
123
|
+
class RecordingNullQueue extends RhiNullQueue {
|
|
124
|
+
readonly writes: CapturedQueueWrite[] = [];
|
|
125
|
+
|
|
126
|
+
override writeBuffer(
|
|
127
|
+
buffer: Buffer,
|
|
128
|
+
bufferOffset: number,
|
|
129
|
+
data: ArrayBufferView | ArrayBuffer,
|
|
130
|
+
dataOffset?: number,
|
|
131
|
+
size?: number,
|
|
132
|
+
) {
|
|
133
|
+
const source =
|
|
134
|
+
data instanceof ArrayBuffer
|
|
135
|
+
? new Uint8Array(data)
|
|
136
|
+
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
137
|
+
const start = dataOffset ?? 0;
|
|
138
|
+
const end = size === undefined ? source.byteLength : start + size;
|
|
139
|
+
this.writes.push({ buffer, offset: bufferOffset, bytes: source.slice(start, end) });
|
|
140
|
+
return super.writeBuffer(buffer, bufferOffset, data, dataOffset, size);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
114
144
|
function surfaceCanvas(width = 1920, height = 1080): HTMLCanvasElement {
|
|
115
145
|
return { width, height, getContext: () => null } as unknown as HTMLCanvasElement;
|
|
116
146
|
}
|
|
@@ -336,11 +366,11 @@ describe('volumetric fog budget and recovery contract', () => {
|
|
|
336
366
|
label: 'volume-params',
|
|
337
367
|
kind: 'buffer',
|
|
338
368
|
origin: 'imported',
|
|
339
|
-
descriptor: { kind: 'buffer', size:
|
|
369
|
+
descriptor: { kind: 'buffer', size: VOLUMETRIC_FOG_PARAMS_BYTES },
|
|
340
370
|
firstUse: 0,
|
|
341
371
|
lastUse: 3,
|
|
342
372
|
derivedUsage: 64,
|
|
343
|
-
byteSize:
|
|
373
|
+
byteSize: VOLUMETRIC_FOG_PARAMS_BYTES,
|
|
344
374
|
},
|
|
345
375
|
],
|
|
346
376
|
} as const;
|
|
@@ -349,21 +379,25 @@ describe('volumetric fog budget and recovery contract', () => {
|
|
|
349
379
|
expect(facts.sampleCount).toBe(960 * 540 * 64);
|
|
350
380
|
expect(facts.resolvedPixelCount).toBe(960 * 540);
|
|
351
381
|
expect(facts.currentBytes).toBe(960 * 540 * 8);
|
|
352
|
-
expect(facts.bufferBytes).toBe(
|
|
382
|
+
expect(facts.bufferBytes).toBe(VOLUMETRIC_FOG_PARAMS_BYTES);
|
|
353
383
|
expect(facts.liveResourceCount).toBe(5);
|
|
354
384
|
expect(facts.historyBytes).toBe(960 * 540 * 8 * 2);
|
|
355
385
|
expect(facts.physicalResourceCount).toBe(5);
|
|
356
|
-
expect(facts.totalBytes).toBe(
|
|
386
|
+
expect(facts.totalBytes).toBe(
|
|
387
|
+
960 * 540 * 16 * 4 + 960 * 540 * 8 * 3 + VOLUMETRIC_FOG_PARAMS_BYTES,
|
|
388
|
+
);
|
|
357
389
|
expect(facts.totalBytes).toBe(
|
|
358
390
|
facts.currentBytes + facts.historyBytes + facts.scratchBytes + facts.bufferBytes,
|
|
359
391
|
);
|
|
360
392
|
expect(facts.totalBytes).toBeLessThanOrEqual(MAX_MEMORY_BYTES);
|
|
361
393
|
|
|
362
394
|
const doubleBuffered = inspectVolumetricFogResources(graph, { parameterBufferCount: 2 });
|
|
363
|
-
expect(doubleBuffered.bufferBytes).toBe(
|
|
395
|
+
expect(doubleBuffered.bufferBytes).toBe(VOLUMETRIC_FOG_PARAMS_BYTES * 2);
|
|
364
396
|
expect(doubleBuffered.liveResourceCount).toBe(6);
|
|
365
397
|
expect(doubleBuffered.physicalResourceCount).toBe(6);
|
|
366
|
-
expect(doubleBuffered.totalBytes).toBe(
|
|
398
|
+
expect(doubleBuffered.totalBytes).toBe(
|
|
399
|
+
960 * 540 * 16 * 4 + 960 * 540 * 8 * 3 + VOLUMETRIC_FOG_PARAMS_BYTES * 2,
|
|
400
|
+
);
|
|
367
401
|
expect(doubleBuffered.totalBytes).toBe(
|
|
368
402
|
doubleBuffered.currentBytes +
|
|
369
403
|
doubleBuffered.historyBytes +
|
|
@@ -372,6 +406,136 @@ describe('volumetric fog budget and recovery contract', () => {
|
|
|
372
406
|
);
|
|
373
407
|
});
|
|
374
408
|
|
|
409
|
+
it('uses the canonical World clock to bound real volume history reuse', async () => {
|
|
410
|
+
const queue = new RecordingNullQueue();
|
|
411
|
+
const adapter = new RecoverableNullAdapter(() => queue);
|
|
412
|
+
const backend = {
|
|
413
|
+
...nullRhi,
|
|
414
|
+
requestAdapter: async () => ok(adapter),
|
|
415
|
+
};
|
|
416
|
+
const created = await constructRendererHost(
|
|
417
|
+
surfaceCanvas(640, 360),
|
|
418
|
+
{ rhi: backend },
|
|
419
|
+
{ shaderManifestUrl: VOLUME_MANIFEST },
|
|
420
|
+
);
|
|
421
|
+
expect(created.ok).toBe(true);
|
|
422
|
+
if (!created.ok) return;
|
|
423
|
+
const { renderer } = created.value;
|
|
424
|
+
const world = new World();
|
|
425
|
+
const density = world.allocSharedRef('TextureAsset', densityAsset());
|
|
426
|
+
world
|
|
427
|
+
.spawn(
|
|
428
|
+
{ component: Transform, data: { pos: [0, 0, 3] } },
|
|
429
|
+
{
|
|
430
|
+
component: Camera,
|
|
431
|
+
data: {
|
|
432
|
+
...perspective({ fov: Math.PI / 4, aspect: 16 / 9 }),
|
|
433
|
+
tonemap: TONEMAP_ACES_FILMIC,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
)
|
|
437
|
+
.unwrap();
|
|
438
|
+
const sun = world
|
|
439
|
+
.spawn({
|
|
440
|
+
component: DirectionalLight,
|
|
441
|
+
data: { direction: [-0.4, -0.8, -0.3], castShadow: true },
|
|
442
|
+
})
|
|
443
|
+
.unwrap();
|
|
444
|
+
world
|
|
445
|
+
.spawn({
|
|
446
|
+
component: VolumetricFog,
|
|
447
|
+
data: {
|
|
448
|
+
light: sun,
|
|
449
|
+
density,
|
|
450
|
+
boundsMin: [-1, -1, -1],
|
|
451
|
+
boundsMax: [1, 1, 1],
|
|
452
|
+
extinction: [0.2, 0.2, 0.2],
|
|
453
|
+
albedo: [0.8, 0.8, 0.8],
|
|
454
|
+
emission: [0, 0, 0],
|
|
455
|
+
anisotropy: 0,
|
|
456
|
+
maxDistance: 50,
|
|
457
|
+
},
|
|
458
|
+
})
|
|
459
|
+
.unwrap();
|
|
460
|
+
|
|
461
|
+
const lease = renderer.attach(world);
|
|
462
|
+
expect(lease.ok).toBe(true);
|
|
463
|
+
if (!lease.ok) return;
|
|
464
|
+
const request = {
|
|
465
|
+
leases: [lease.value],
|
|
466
|
+
camera: { lease: lease.value },
|
|
467
|
+
environment: { lease: lease.value },
|
|
468
|
+
} as const;
|
|
469
|
+
const drawAndRead = async (): Promise<{
|
|
470
|
+
readonly history: number;
|
|
471
|
+
readonly worldTime: number;
|
|
472
|
+
readonly delta: number;
|
|
473
|
+
}> => {
|
|
474
|
+
queue.writes.length = 0;
|
|
475
|
+
const frame = renderer.draw(request);
|
|
476
|
+
expect(frame.ok).toBe(true);
|
|
477
|
+
if (!frame.ok) return { history: -1, worldTime: -1, delta: -1 };
|
|
478
|
+
expect((await frame.value.completed).ok).toBe(true);
|
|
479
|
+
const paramsWrite = queue.writes
|
|
480
|
+
.filter((write) => write.offset === 0 && write.bytes.byteLength === 144)
|
|
481
|
+
.at(-1);
|
|
482
|
+
expect(paramsWrite).toBeDefined();
|
|
483
|
+
if (paramsWrite === undefined) return { history: -1, worldTime: -1, delta: -1 };
|
|
484
|
+
const historyWrite = queue.writes
|
|
485
|
+
.filter(
|
|
486
|
+
(write) =>
|
|
487
|
+
write.buffer === paramsWrite.buffer &&
|
|
488
|
+
write.offset === 31 * Float32Array.BYTES_PER_ELEMENT &&
|
|
489
|
+
write.bytes.byteLength === 3 * Float32Array.BYTES_PER_ELEMENT,
|
|
490
|
+
)
|
|
491
|
+
.at(-1);
|
|
492
|
+
expect(historyWrite).toBeDefined();
|
|
493
|
+
if (historyWrite === undefined) return { history: -1, worldTime: -1, delta: -1 };
|
|
494
|
+
return {
|
|
495
|
+
history: new Float32Array(
|
|
496
|
+
historyWrite.bytes.buffer,
|
|
497
|
+
historyWrite.bytes.byteOffset,
|
|
498
|
+
1,
|
|
499
|
+
)[0] as number,
|
|
500
|
+
worldTime: new Float32Array(
|
|
501
|
+
historyWrite.bytes.buffer,
|
|
502
|
+
historyWrite.bytes.byteOffset,
|
|
503
|
+
3,
|
|
504
|
+
)[1] as number,
|
|
505
|
+
delta: new Float32Array(
|
|
506
|
+
historyWrite.bytes.buffer,
|
|
507
|
+
historyWrite.bytes.byteOffset,
|
|
508
|
+
3,
|
|
509
|
+
)[2] as number,
|
|
510
|
+
};
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
world.update(1 / 60).unwrap();
|
|
514
|
+
const firstTime = world.getResource(Time).elapsed;
|
|
515
|
+
const first = await drawAndRead();
|
|
516
|
+
expect(first.worldTime).toBeCloseTo(firstTime, 5);
|
|
517
|
+
expect(first.history).toBe(0);
|
|
518
|
+
|
|
519
|
+
const stable = await drawAndRead();
|
|
520
|
+
expect(world.getResource(Time).elapsed).toBe(firstTime);
|
|
521
|
+
expect(stable.worldTime).toBeCloseTo(firstTime, 5);
|
|
522
|
+
expect(stable.history).toBe(1);
|
|
523
|
+
|
|
524
|
+
world.update(1 / 60).unwrap();
|
|
525
|
+
const movedTime = world.getResource(Time).elapsed;
|
|
526
|
+
const moved = await drawAndRead();
|
|
527
|
+
expect(moved.worldTime).toBeCloseTo(movedTime, 5);
|
|
528
|
+
expect(moved.worldTime).toBeGreaterThan(first.worldTime);
|
|
529
|
+
expect(moved.history).toBe(1);
|
|
530
|
+
expect(moved.delta).toBeCloseTo(1 / 60, 6);
|
|
531
|
+
expect(stable.delta).toBe(0);
|
|
532
|
+
for (let index = 0; index < 12; index += 1) world.update(1 / 60).unwrap();
|
|
533
|
+
const jumped = await drawAndRead();
|
|
534
|
+
expect(jumped.history).toBe(0);
|
|
535
|
+
expect(jumped.delta).toBeGreaterThan(0.1);
|
|
536
|
+
await renderer.dispose();
|
|
537
|
+
}, 30_000);
|
|
538
|
+
|
|
375
539
|
it('derives a same-XY resolve descriptor when the packed raw extent is supplied alone', async () => {
|
|
376
540
|
const adapter = await nullRhi.requestAdapter();
|
|
377
541
|
expect(adapter.ok).toBe(true);
|