@forgeax/engine-runtime 0.1.28 → 0.1.29
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 +98 -0
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +480 -15
- package/dist/index.mjs.map +1 -1
- package/dist/scene-utils/mount-override-fold.d.ts +3 -1
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
- package/package.json +37 -32
- package/src/__tests__/animation-graph-collect-roundtrip.test.ts +4 -2
- package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +5 -4
- package/src/__tests__/asset-registry-guid-reverse.test.ts +22 -27
- package/src/__tests__/asset-registry-mounts.test.ts +174 -734
- package/src/__tests__/asset-registry-scene.cases.unit.test.ts +9 -18
- package/src/__tests__/asset-registry.recursive.spec.ts +23 -29
- package/src/__tests__/camera-ortho.cases.unit.test.ts +12 -3
- package/src/__tests__/collect-scene-asset.test.ts +96 -113
- package/src/__tests__/collect-scene-mount-collapse.test.ts +64 -66
- package/src/__tests__/collect-transient-children-absent.test.ts +11 -17
- package/src/__tests__/collect-transient-roundtrip.test.ts +12 -13
- package/src/__tests__/collect-transient-skip.test.ts +20 -27
- package/src/__tests__/components.test-d.ts +17 -1
- package/src/__tests__/components.unit.test.ts +75 -27
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +4 -2
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +8 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +4 -1
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +66 -92
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +200 -4
- package/src/__tests__/errors.unit.test.ts +13 -0
- package/src/__tests__/feature-depth-material.dawn.test.ts +23 -0
- package/src/__tests__/feature-depth-material.fixture.ts +473 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +14 -14
- package/src/__tests__/geometry.unit.test.ts +42 -43
- package/src/__tests__/gpu-resource-release.cases.unit.test.ts +16 -9
- package/src/__tests__/handle-migration.test.ts +0 -1
- package/src/__tests__/instances-renderer-lifecycle.integration.test.ts +20 -14
- package/src/__tests__/instances.test-d.ts +13 -52
- package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +42 -80
- package/src/__tests__/lazy-catalog.cases.unit.test.ts +17 -3
- package/src/__tests__/loader-registry.cases.unit.test.ts +4 -4
- package/src/__tests__/material-publication.browser.test.ts +29 -0
- package/src/__tests__/material-publication.commands.ts +23 -0
- package/src/__tests__/material-publication.dawn.test.ts +24 -0
- package/src/__tests__/material-publication.fixture.ts +349 -0
- package/src/__tests__/material-publication.server.ts +109 -0
- package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +73 -96
- package/src/__tests__/pbr-pipeline.unit.test.ts +25 -11
- package/src/__tests__/point-light-shadow.browser.test.ts +8 -2
- package/src/__tests__/point-light-shadow.unit.test.ts +37 -8
- package/src/__tests__/record-all-topology.cases.unit.test.ts +52 -0
- package/src/__tests__/record-draw-branch.cases.unit.test.ts +1 -0
- package/src/__tests__/record-worldid-isolation.test.ts +47 -28
- package/src/__tests__/render-error-exhaustive.test-d.ts +24 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +15 -4
- package/src/__tests__/render-system-extract.test.ts +61 -0
- package/src/__tests__/render-system-extract.unit.test.ts +21 -7
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +33 -1
- package/src/__tests__/render-system-fold-material.unit.test.ts +32 -6
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +26 -4
- package/src/__tests__/render-system-fold.dawn.test.ts +4 -1
- package/src/__tests__/render-system-mega.test.ts +50 -62
- package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +0 -1
- package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +0 -3
- package/src/__tests__/renderer-draw-world.cases.unit.test.ts +16 -513
- package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +48 -43
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +9 -7
- package/src/__tests__/roots-to-scene-asset.test.ts +27 -28
- package/src/__tests__/roundtrip-primitive.test.ts +24 -23
- package/src/__tests__/scene-equirect-roundtrip.test.ts +5 -16
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +13 -29
- package/src/__tests__/scene-mount-fixed-point.test.ts +104 -227
- package/src/__tests__/scene-mount-roundtrip.test.ts +95 -961
- package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +36 -182
- package/src/__tests__/scene-sugar-instantiate.test.ts +20 -15
- package/src/__tests__/serialize-mounts-roundtrip.test.ts +108 -492
- package/src/__tests__/serialize-scene-asset.test.ts +36 -50
- package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +34 -19
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +5 -12
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +6 -4
- package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-palette-extract.cases.unit.test.ts +2 -0
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +93 -48
- package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +6 -2
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +1 -1
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -3
- package/src/__tests__/template-game-default-api-contract.test.ts +21 -21
- package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +8 -5
- package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +3 -0
- package/src/__tests__/transient-fields-collect.test.ts +4 -4
- package/src/__tests__/transient-runtime-behavior.test.ts +9 -9
- package/src/__tests__/vfx-depth-material.dawn.test.ts +42 -0
- package/src/__tests__/vfx-mesh-lighting.browser.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.commands.ts +14 -0
- package/src/__tests__/vfx-mesh-lighting.dawn.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.fixture.ts +883 -0
- package/src/__tests__/wave1-rendering-materials.browser.test.ts +617 -0
- package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
- package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
- package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
- package/src/__tests__/world-scene-despawn.test.ts +8 -8
- package/src/__tests__/world-scene-instantiate.test.ts +23 -29
- package/src/__tests__/world-scene-nested.test.ts +90 -163
- package/src/__tests__/world-scene-override.test.ts +17 -16
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
- package/src/collect-scene-asset.ts +637 -19
- package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
- package/src/index.ts +1 -0
- package/src/scene-utils/mount-override-fold.ts +5 -14
- package/src/__tests__/node-shims.d.ts +0 -31
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import { createWorldContext, type EntityHandle, FixedTime, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import {
|
|
3
|
+
ANTIALIAS_FXAA,
|
|
4
|
+
ANTIALIAS_NONE,
|
|
5
|
+
ANTIALIAS_TAA,
|
|
6
|
+
Atmosphere,
|
|
7
|
+
Camera,
|
|
8
|
+
DirectionalLight,
|
|
9
|
+
type DynamicGeometryCandidate,
|
|
10
|
+
type FrameReceipt,
|
|
11
|
+
MeshFilter,
|
|
12
|
+
type Renderer,
|
|
13
|
+
type RenderWorldLease,
|
|
14
|
+
} from '@forgeax/engine-render';
|
|
15
|
+
import { propagateTransforms, scenePlugin, Transform } from '@forgeax/engine-scene';
|
|
16
|
+
import type { Handle, MeshAsset } from '@forgeax/engine-types';
|
|
17
|
+
import { expect, it, vi } from 'vitest';
|
|
18
|
+
import { page } from 'vitest/browser';
|
|
19
|
+
import { createWave1RenderingRecipe } from '../../../../scripts/dev-verify/wave1-rendering/recipe';
|
|
20
|
+
import { createRenderer } from '../createRenderer';
|
|
21
|
+
|
|
22
|
+
interface ScreenshotPixels {
|
|
23
|
+
readonly width: number;
|
|
24
|
+
readonly height: number;
|
|
25
|
+
readonly pixels: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PixelStats {
|
|
29
|
+
readonly width: number;
|
|
30
|
+
readonly height: number;
|
|
31
|
+
readonly nonBlackPixels: number;
|
|
32
|
+
readonly meanLuma: number;
|
|
33
|
+
readonly maxChannel: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PixelDelta {
|
|
37
|
+
readonly changedPixels: number;
|
|
38
|
+
readonly meanAbsoluteChannelDelta: number;
|
|
39
|
+
readonly maxChannelDelta: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function screenshotPixels(
|
|
43
|
+
canvas: HTMLCanvasElement,
|
|
44
|
+
artifactName?: string,
|
|
45
|
+
): Promise<ScreenshotPixels> {
|
|
46
|
+
const shot =
|
|
47
|
+
artifactName === undefined
|
|
48
|
+
? await page.elementLocator(canvas).screenshot({ base64: true, save: false })
|
|
49
|
+
: await page.elementLocator(canvas).screenshot({
|
|
50
|
+
path: `../../../../artifacts/wave1-rendering/${artifactName}.png`,
|
|
51
|
+
base64: true,
|
|
52
|
+
});
|
|
53
|
+
const base64 = typeof shot === 'string' ? shot : shot.base64;
|
|
54
|
+
const binary = atob(base64);
|
|
55
|
+
const bytes = new Uint8Array(binary.length);
|
|
56
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
57
|
+
bytes[index] = binary.charCodeAt(index);
|
|
58
|
+
}
|
|
59
|
+
const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
|
|
60
|
+
const offscreen = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
61
|
+
const context = offscreen.getContext('2d', { willReadFrequently: true });
|
|
62
|
+
if (context === null) {
|
|
63
|
+
bitmap.close();
|
|
64
|
+
throw new Error('wave1-rendering: screenshot pixel context unavailable');
|
|
65
|
+
}
|
|
66
|
+
context.drawImage(bitmap, 0, 0);
|
|
67
|
+
const image = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
68
|
+
bitmap.close();
|
|
69
|
+
return {
|
|
70
|
+
width: image.width,
|
|
71
|
+
height: image.height,
|
|
72
|
+
pixels: new Uint8Array(image.data),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pixelStats(readback: ScreenshotPixels): PixelStats {
|
|
77
|
+
let nonBlackPixels = 0;
|
|
78
|
+
let lumaTotal = 0;
|
|
79
|
+
let maxChannel = 0;
|
|
80
|
+
for (let offset = 0; offset < readback.pixels.length; offset += 4) {
|
|
81
|
+
const red = readback.pixels[offset] ?? 0;
|
|
82
|
+
const green = readback.pixels[offset + 1] ?? 0;
|
|
83
|
+
const blue = readback.pixels[offset + 2] ?? 0;
|
|
84
|
+
const brightness = red + green + blue;
|
|
85
|
+
if (brightness > 3) nonBlackPixels += 1;
|
|
86
|
+
lumaTotal += 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
|
87
|
+
maxChannel = Math.max(maxChannel, red, green, blue);
|
|
88
|
+
}
|
|
89
|
+
const pixelCount = Math.max(1, readback.width * readback.height);
|
|
90
|
+
return {
|
|
91
|
+
width: readback.width,
|
|
92
|
+
height: readback.height,
|
|
93
|
+
nonBlackPixels,
|
|
94
|
+
meanLuma: lumaTotal / pixelCount,
|
|
95
|
+
maxChannel,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pixelDelta(left: ScreenshotPixels, right: ScreenshotPixels): PixelDelta {
|
|
100
|
+
if (left.width !== right.width || left.height !== right.height) {
|
|
101
|
+
return {
|
|
102
|
+
changedPixels: Math.max(left.width * left.height, right.width * right.height),
|
|
103
|
+
meanAbsoluteChannelDelta: 255,
|
|
104
|
+
maxChannelDelta: 255,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let changedPixels = 0;
|
|
108
|
+
let absoluteDelta = 0;
|
|
109
|
+
let maxChannelDelta = 0;
|
|
110
|
+
for (let offset = 0; offset < left.pixels.length; offset += 4) {
|
|
111
|
+
let changed = false;
|
|
112
|
+
for (let channel = 0; channel < 3; channel += 1) {
|
|
113
|
+
const delta = Math.abs(
|
|
114
|
+
(left.pixels[offset + channel] ?? 0) - (right.pixels[offset + channel] ?? 0),
|
|
115
|
+
);
|
|
116
|
+
if (delta > 0) changed = true;
|
|
117
|
+
absoluteDelta += delta;
|
|
118
|
+
maxChannelDelta = Math.max(maxChannelDelta, delta);
|
|
119
|
+
}
|
|
120
|
+
if (changed) changedPixels += 1;
|
|
121
|
+
}
|
|
122
|
+
const pixelCount = Math.max(1, left.width * left.height * 3);
|
|
123
|
+
return {
|
|
124
|
+
changedPixels,
|
|
125
|
+
meanAbsoluteChannelDelta: absoluteDelta / pixelCount,
|
|
126
|
+
maxChannelDelta,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Copy a predictable screen region so sky evidence is not inferred from a full-frame delta. */
|
|
131
|
+
function cropPixels(
|
|
132
|
+
readback: ScreenshotPixels,
|
|
133
|
+
centerX: number,
|
|
134
|
+
centerY: number,
|
|
135
|
+
halfWidth: number,
|
|
136
|
+
halfHeight: number,
|
|
137
|
+
): ScreenshotPixels {
|
|
138
|
+
const x0 = Math.max(0, Math.floor((centerX - halfWidth) * readback.width));
|
|
139
|
+
const y0 = Math.max(0, Math.floor((centerY - halfHeight) * readback.height));
|
|
140
|
+
const x1 = Math.min(readback.width, Math.ceil((centerX + halfWidth) * readback.width));
|
|
141
|
+
const y1 = Math.min(readback.height, Math.ceil((centerY + halfHeight) * readback.height));
|
|
142
|
+
const width = Math.max(1, x1 - x0);
|
|
143
|
+
const height = Math.max(1, y1 - y0);
|
|
144
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
145
|
+
for (let row = 0; row < height; row += 1) {
|
|
146
|
+
const sourceOffset = ((y0 + row) * readback.width + x0) * 4;
|
|
147
|
+
const targetOffset = row * width * 4;
|
|
148
|
+
pixels.set(readback.pixels.subarray(sourceOffset, sourceOffset + width * 4), targetOffset);
|
|
149
|
+
}
|
|
150
|
+
return { width, height, pixels };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function inspectionStats(inspection: ReturnType<Renderer['inspect']>): Record<string, unknown> {
|
|
154
|
+
const gpu = inspection.renderScene.gpu;
|
|
155
|
+
return {
|
|
156
|
+
state: inspection.state,
|
|
157
|
+
frame: inspection.frame,
|
|
158
|
+
passes: inspection.perFramePassNames,
|
|
159
|
+
environment: inspection.environment,
|
|
160
|
+
temporal: inspection.temporal,
|
|
161
|
+
directionalShadow: inspection.directionalShadow,
|
|
162
|
+
dynamicGeometry: inspection.dynamicGeometry,
|
|
163
|
+
renderScene: {
|
|
164
|
+
projectionRecords: inspection.renderScene.projectionRecords,
|
|
165
|
+
gpu,
|
|
166
|
+
gpuDriven: inspection.renderScene.gpuDriven,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function waitForFrame(
|
|
172
|
+
world: World,
|
|
173
|
+
renderer: Renderer,
|
|
174
|
+
lease: RenderWorldLease,
|
|
175
|
+
canvas: HTMLCanvasElement,
|
|
176
|
+
label: string,
|
|
177
|
+
observe: boolean,
|
|
178
|
+
artifactName?: string,
|
|
179
|
+
cameraEntity?: number,
|
|
180
|
+
) {
|
|
181
|
+
const receipt = await submitFrame(world, renderer, lease, cameraEntity);
|
|
182
|
+
const observation = observe
|
|
183
|
+
? await renderer.observe(receipt, { include: ['timings', 'draws', 'bindings'] })
|
|
184
|
+
: undefined;
|
|
185
|
+
if (observation !== undefined && !observation.ok) throw observation.error;
|
|
186
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
187
|
+
const pixels = await screenshotPixels(canvas, artifactName);
|
|
188
|
+
const inspection = renderer.inspect();
|
|
189
|
+
// biome-ignore lint/suspicious/noConsole: frame evidence is intentionally emitted for dev verification.
|
|
190
|
+
console.info(
|
|
191
|
+
`[wave1-rendering:p0] ${label}`,
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
pixel: pixelStats(pixels),
|
|
194
|
+
frame: receipt.frameId,
|
|
195
|
+
receipt: {
|
|
196
|
+
frameId: receipt.frameId,
|
|
197
|
+
deviceGeneration: receipt.deviceGeneration,
|
|
198
|
+
completed: true,
|
|
199
|
+
},
|
|
200
|
+
observation:
|
|
201
|
+
observation?.ok === true
|
|
202
|
+
? {
|
|
203
|
+
frameId: observation.value.frameId,
|
|
204
|
+
include: observation.value.include,
|
|
205
|
+
timings: observation.value.timings?.status ?? 'omitted',
|
|
206
|
+
}
|
|
207
|
+
: 'omitted',
|
|
208
|
+
resources: inspectionStats(inspection),
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
return { receipt, pixels, inspection, observation };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function submitFrame(
|
|
215
|
+
world: World,
|
|
216
|
+
renderer: Renderer,
|
|
217
|
+
lease: RenderWorldLease,
|
|
218
|
+
cameraEntity?: number,
|
|
219
|
+
): Promise<FrameReceipt> {
|
|
220
|
+
propagateTransforms(world).unwrap();
|
|
221
|
+
const drawn = renderer.draw({
|
|
222
|
+
leases: [lease],
|
|
223
|
+
camera: { lease, ...(cameraEntity === undefined ? {} : { entityKey: cameraEntity }) },
|
|
224
|
+
environment: { lease },
|
|
225
|
+
fixedStep: world.getResource(FixedTime).tick,
|
|
226
|
+
});
|
|
227
|
+
if (!drawn.ok) throw drawn.error;
|
|
228
|
+
const receipt = drawn.value;
|
|
229
|
+
const completed = await receipt.completed;
|
|
230
|
+
if (!completed.ok) throw completed.error;
|
|
231
|
+
return receipt;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function prepareDynamic(
|
|
235
|
+
renderer: Renderer,
|
|
236
|
+
world: World,
|
|
237
|
+
entity: EntityHandle,
|
|
238
|
+
mesh: MeshAsset,
|
|
239
|
+
meshHandle: Handle<'MeshAsset', 'shared'>,
|
|
240
|
+
revision: number,
|
|
241
|
+
): DynamicGeometryCandidate {
|
|
242
|
+
return renderer
|
|
243
|
+
.prepareDynamicGeometry({
|
|
244
|
+
world,
|
|
245
|
+
entity,
|
|
246
|
+
mesh,
|
|
247
|
+
meshHandle,
|
|
248
|
+
revision,
|
|
249
|
+
})
|
|
250
|
+
.unwrap();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function acceptDynamic(
|
|
254
|
+
renderer: Renderer,
|
|
255
|
+
world: World,
|
|
256
|
+
candidate: DynamicGeometryCandidate,
|
|
257
|
+
): DynamicGeometryCandidate {
|
|
258
|
+
return renderer
|
|
259
|
+
.acceptDynamicGeometry(candidate, {
|
|
260
|
+
world,
|
|
261
|
+
fixedStep: world.getResource(FixedTime).tick,
|
|
262
|
+
})
|
|
263
|
+
.unwrap();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
it('proves the public Atmosphere toggle in the presented sky pixels', async () => {
|
|
267
|
+
const canvas = document.createElement('canvas');
|
|
268
|
+
canvas.width = 256;
|
|
269
|
+
canvas.height = 256;
|
|
270
|
+
canvas.style.width = '256px';
|
|
271
|
+
canvas.style.height = '256px';
|
|
272
|
+
document.body.appendChild(canvas);
|
|
273
|
+
const rendererResult = await createRenderer(
|
|
274
|
+
canvas,
|
|
275
|
+
{},
|
|
276
|
+
{ shaderManifestUrl: '/shaders/manifest.json' },
|
|
277
|
+
);
|
|
278
|
+
if (!rendererResult.ok) throw rendererResult.error;
|
|
279
|
+
const renderer = rendererResult.value;
|
|
280
|
+
const world = new World();
|
|
281
|
+
const scene = await createWorldContext(world, [scenePlugin()]);
|
|
282
|
+
const recipe = createWave1RenderingRecipe(world, { aspect: 1, includeAtmosphere: true });
|
|
283
|
+
const attached = renderer.attach(world);
|
|
284
|
+
if (!attached.ok) throw attached.error;
|
|
285
|
+
try {
|
|
286
|
+
world.update(1 / 60).unwrap();
|
|
287
|
+
const withAtmosphere = await waitForFrame(
|
|
288
|
+
world,
|
|
289
|
+
renderer,
|
|
290
|
+
attached.value,
|
|
291
|
+
canvas,
|
|
292
|
+
'atmosphere-on',
|
|
293
|
+
true,
|
|
294
|
+
'wave1-p0-atmosphere-on',
|
|
295
|
+
);
|
|
296
|
+
const atmosphere = recipe.entities.atmosphere;
|
|
297
|
+
if (atmosphere === undefined)
|
|
298
|
+
throw new Error('wave1-rendering: Atmosphere fixture entity missing');
|
|
299
|
+
recipe.destroy(atmosphere);
|
|
300
|
+
world.update(1 / 60).unwrap();
|
|
301
|
+
const withoutAtmosphere = await waitForFrame(
|
|
302
|
+
world,
|
|
303
|
+
renderer,
|
|
304
|
+
attached.value,
|
|
305
|
+
canvas,
|
|
306
|
+
'atmosphere-off',
|
|
307
|
+
true,
|
|
308
|
+
'wave1-p0-atmosphere-off',
|
|
309
|
+
);
|
|
310
|
+
const skyDelta = pixelDelta(withAtmosphere.pixels, withoutAtmosphere.pixels);
|
|
311
|
+
expect(skyDelta.changedPixels).toBeGreaterThan(0);
|
|
312
|
+
} finally {
|
|
313
|
+
await renderer.dispose();
|
|
314
|
+
recipe.dispose();
|
|
315
|
+
await scene.fiber.dispose();
|
|
316
|
+
canvas.remove();
|
|
317
|
+
}
|
|
318
|
+
}, 120000);
|
|
319
|
+
|
|
320
|
+
it('proves a visible sun disc and wall occlusion through a sky-only view', async () => {
|
|
321
|
+
const canvas = document.createElement('canvas');
|
|
322
|
+
canvas.width = 256;
|
|
323
|
+
canvas.height = 256;
|
|
324
|
+
canvas.style.width = '256px';
|
|
325
|
+
canvas.style.height = '256px';
|
|
326
|
+
document.body.appendChild(canvas);
|
|
327
|
+
const rendererResult = await createRenderer(
|
|
328
|
+
canvas,
|
|
329
|
+
{},
|
|
330
|
+
{ shaderManifestUrl: '/shaders/manifest.json' },
|
|
331
|
+
);
|
|
332
|
+
if (!rendererResult.ok) throw rendererResult.error;
|
|
333
|
+
const renderer = rendererResult.value;
|
|
334
|
+
const world = new World();
|
|
335
|
+
const scene = await createWorldContext(world, [scenePlugin()]);
|
|
336
|
+
const recipe = createWave1RenderingRecipe(world, { aspect: 1, includeAtmosphere: true });
|
|
337
|
+
const attached = renderer.attach(world);
|
|
338
|
+
if (!attached.ok) throw attached.error;
|
|
339
|
+
const lease = attached.value;
|
|
340
|
+
try {
|
|
341
|
+
const atmosphere = recipe.entities.atmosphere;
|
|
342
|
+
if (atmosphere === undefined) {
|
|
343
|
+
throw new Error('wave1-rendering: Atmosphere fixture entity missing for sun proof');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// The component direction is the incoming light vector. Extraction turns
|
|
347
|
+
// it into a view-space sun direction, so this points above the camera's -Z
|
|
348
|
+
// view. Remove every mesh before the first sky-only frame to exercise the
|
|
349
|
+
// camera UBO demand with no scene geometry.
|
|
350
|
+
world.set(recipe.entities.sun, DirectionalLight, { direction: [0, -0.25, 1] }).unwrap();
|
|
351
|
+
world.set(atmosphere, Atmosphere, { sunAngularRadius: 0 }).unwrap();
|
|
352
|
+
recipe.destroy(recipe.entities.wall);
|
|
353
|
+
recipe.destroy(recipe.entities.floor);
|
|
354
|
+
recipe.destroy(recipe.entities.screen);
|
|
355
|
+
recipe.destroy(recipe.entities.roof);
|
|
356
|
+
world.update(1 / 60).unwrap();
|
|
357
|
+
const visibleBaseline = await waitForFrame(
|
|
358
|
+
world,
|
|
359
|
+
renderer,
|
|
360
|
+
lease,
|
|
361
|
+
canvas,
|
|
362
|
+
'sun-visible-no-disc',
|
|
363
|
+
true,
|
|
364
|
+
);
|
|
365
|
+
world.set(atmosphere, Atmosphere, { sunAngularRadius: 0.03 }).unwrap();
|
|
366
|
+
world.update(1 / 60).unwrap();
|
|
367
|
+
const visibleSun = await waitForFrame(
|
|
368
|
+
world,
|
|
369
|
+
renderer,
|
|
370
|
+
lease,
|
|
371
|
+
canvas,
|
|
372
|
+
'sun-visible',
|
|
373
|
+
true,
|
|
374
|
+
'wave1-p0-sun-visible',
|
|
375
|
+
);
|
|
376
|
+
// With direction [0,-0.25,1], the projected disc is in this upper sky
|
|
377
|
+
// region; comparing this ROI catches the actual bright disc rather than
|
|
378
|
+
// treating unrelated full-frame atmosphere changes as sun evidence.
|
|
379
|
+
const visibleSkyRoi = (pixels: ScreenshotPixels) => cropPixels(pixels, 0.5, 0.28, 0.14, 0.12);
|
|
380
|
+
const visibleDelta = pixelDelta(
|
|
381
|
+
visibleSkyRoi(visibleBaseline.pixels),
|
|
382
|
+
visibleSkyRoi(visibleSun.pixels),
|
|
383
|
+
);
|
|
384
|
+
expect(visibleDelta.changedPixels).toBeGreaterThan(0);
|
|
385
|
+
expect(visibleDelta.meanAbsoluteChannelDelta).toBeGreaterThan(0);
|
|
386
|
+
expect(visibleDelta.maxChannelDelta).toBeGreaterThan(0);
|
|
387
|
+
|
|
388
|
+
// Put the sun just above the view axis and create one opaque wall in its
|
|
389
|
+
// projected path. The paired radius-zero frame keeps exposure, atmosphere,
|
|
390
|
+
// and indoor materials identical while the center ROI checks occlusion.
|
|
391
|
+
world.set(recipe.entities.sun, DirectionalLight, { direction: [0, -0.01, 1] }).unwrap();
|
|
392
|
+
const occluder = recipe.spawnReplacement();
|
|
393
|
+
world.set(atmosphere, Atmosphere, { sunAngularRadius: 0 }).unwrap();
|
|
394
|
+
world.update(1 / 60).unwrap();
|
|
395
|
+
const occludedBaseline = await waitForFrame(
|
|
396
|
+
world,
|
|
397
|
+
renderer,
|
|
398
|
+
lease,
|
|
399
|
+
canvas,
|
|
400
|
+
'sun-occluded-no-disc',
|
|
401
|
+
true,
|
|
402
|
+
);
|
|
403
|
+
world.set(atmosphere, Atmosphere, { sunAngularRadius: 0.03 }).unwrap();
|
|
404
|
+
world.update(1 / 60).unwrap();
|
|
405
|
+
const occludedSun = await waitForFrame(
|
|
406
|
+
world,
|
|
407
|
+
renderer,
|
|
408
|
+
lease,
|
|
409
|
+
canvas,
|
|
410
|
+
'sun-occluded',
|
|
411
|
+
true,
|
|
412
|
+
'wave1-p0-sun-occluded',
|
|
413
|
+
);
|
|
414
|
+
const wallRoi = (pixels: ScreenshotPixels) => cropPixels(pixels, 0.5, 0.5, 0.1, 0.1);
|
|
415
|
+
const occludedDelta = pixelDelta(wallRoi(occludedBaseline.pixels), wallRoi(occludedSun.pixels));
|
|
416
|
+
expect(occludedDelta.changedPixels).toBe(0);
|
|
417
|
+
recipe.destroy(occluder);
|
|
418
|
+
} finally {
|
|
419
|
+
await renderer.dispose();
|
|
420
|
+
recipe.dispose();
|
|
421
|
+
await scene.fiber.dispose();
|
|
422
|
+
canvas.remove();
|
|
423
|
+
}
|
|
424
|
+
}, 120000);
|
|
425
|
+
|
|
426
|
+
it('publishes the Wave1 P0 room through real receipts, pixels, and resource evidence', async () => {
|
|
427
|
+
// CI checks the same transitions and a complete movement cycle at a smaller
|
|
428
|
+
// software target. The default run retains the 300-frame endurance evidence.
|
|
429
|
+
const lightweight = import.meta.env.FORGEAX_BROWSER_CI_LIGHTWEIGHT === '1';
|
|
430
|
+
const steadyFrames = lightweight ? 24 : 300;
|
|
431
|
+
const initialSize = lightweight ? 192 : 384;
|
|
432
|
+
const resizedCanvas = lightweight ? { width: 160, height: 120 } : { width: 320, height: 240 };
|
|
433
|
+
const canvas = document.createElement('canvas');
|
|
434
|
+
canvas.width = initialSize;
|
|
435
|
+
canvas.height = initialSize;
|
|
436
|
+
canvas.style.width = `${initialSize}px`;
|
|
437
|
+
canvas.style.height = `${initialSize}px`;
|
|
438
|
+
document.body.appendChild(canvas);
|
|
439
|
+
|
|
440
|
+
const rendererResult = await createRenderer(
|
|
441
|
+
canvas,
|
|
442
|
+
{},
|
|
443
|
+
{ shaderManifestUrl: '/shaders/manifest.json' },
|
|
444
|
+
);
|
|
445
|
+
if (!rendererResult.ok) throw rendererResult.error;
|
|
446
|
+
const renderer = rendererResult.value;
|
|
447
|
+
const world = new World();
|
|
448
|
+
const scene = await createWorldContext(world, [scenePlugin()]);
|
|
449
|
+
const recipe = createWave1RenderingRecipe(world, { aspect: 1 });
|
|
450
|
+
const attached = renderer.attach(world);
|
|
451
|
+
if (!attached.ok) throw attached.error;
|
|
452
|
+
const lease = attached.value;
|
|
453
|
+
const worldErrors = vi.spyOn(console, 'error');
|
|
454
|
+
const errors: string[] = [];
|
|
455
|
+
const unsubscribe = renderer.subscribe((event) => {
|
|
456
|
+
if (event.kind === 'error') errors.push(event.error.code);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
world.update(1 / 60).unwrap();
|
|
461
|
+
const first = await waitForFrame(world, renderer, lease, canvas, 'closed', true);
|
|
462
|
+
expect(first.pixels.width).toBeGreaterThan(0);
|
|
463
|
+
expect(first.pixels.height).toBeGreaterThan(0);
|
|
464
|
+
expect(pixelStats(first.pixels).nonBlackPixels).toBeGreaterThan(0);
|
|
465
|
+
expect(first.inspection.directionalShadow.requested).not.toBe('off');
|
|
466
|
+
expect(first.inspection.directionalShadow.mapSize).toBe(1024);
|
|
467
|
+
expect(first.inspection.frame.frameId).toBe(first.receipt.frameId);
|
|
468
|
+
|
|
469
|
+
const aperturePrepared = prepareDynamic(
|
|
470
|
+
renderer,
|
|
471
|
+
world,
|
|
472
|
+
recipe.entities.wall,
|
|
473
|
+
recipe.assets.wallAperture,
|
|
474
|
+
recipe.assets.wallApertureHandle,
|
|
475
|
+
1,
|
|
476
|
+
);
|
|
477
|
+
expect(world.get(recipe.entities.wall, MeshFilter).unwrap().assetHandle).toBe(
|
|
478
|
+
recipe.assets.wallClosedHandle,
|
|
479
|
+
);
|
|
480
|
+
const prepared = await waitForFrame(world, renderer, lease, canvas, 'prepared', true);
|
|
481
|
+
expect(pixelDelta(first.pixels, prepared.pixels).changedPixels).toBe(0);
|
|
482
|
+
|
|
483
|
+
const apertureCandidate = acceptDynamic(renderer, world, aperturePrepared);
|
|
484
|
+
const opened = await waitForFrame(
|
|
485
|
+
world,
|
|
486
|
+
renderer,
|
|
487
|
+
lease,
|
|
488
|
+
canvas,
|
|
489
|
+
'aperture-published',
|
|
490
|
+
true,
|
|
491
|
+
'wave1-p0-aperture',
|
|
492
|
+
);
|
|
493
|
+
expect(pixelDelta(first.pixels, opened.pixels).changedPixels).toBeGreaterThan(0);
|
|
494
|
+
expect(renderer.dynamicGeometryReceipt(apertureCandidate)?.frameId).toBe(
|
|
495
|
+
opened.receipt.frameId,
|
|
496
|
+
);
|
|
497
|
+
expect(opened.inspection.dynamicGeometry?.published).toBeGreaterThanOrEqual(1);
|
|
498
|
+
expect(opened.inspection.renderScene.projectionRecords).toBeGreaterThan(0);
|
|
499
|
+
|
|
500
|
+
const successorA = recipe.spawnSuccessor([-2.1, 0, 0]);
|
|
501
|
+
const successorB = recipe.spawnSuccessor([2.1, 0, 0]);
|
|
502
|
+
const successors = await waitForFrame(
|
|
503
|
+
world,
|
|
504
|
+
renderer,
|
|
505
|
+
lease,
|
|
506
|
+
canvas,
|
|
507
|
+
'successors-spawned',
|
|
508
|
+
true,
|
|
509
|
+
);
|
|
510
|
+
recipe.setPosition(successorA, [-1.55, 0.25, -0.25]);
|
|
511
|
+
recipe.setPosition(successorB, [1.55, -0.15, -0.25]);
|
|
512
|
+
world.update(1 / 60).unwrap();
|
|
513
|
+
const movedSuccessors = await waitForFrame(
|
|
514
|
+
world,
|
|
515
|
+
renderer,
|
|
516
|
+
lease,
|
|
517
|
+
canvas,
|
|
518
|
+
'successors-moved',
|
|
519
|
+
true,
|
|
520
|
+
'wave1-p0-successors',
|
|
521
|
+
);
|
|
522
|
+
expect(pixelDelta(successors.pixels, movedSuccessors.pixels).changedPixels).toBeGreaterThan(0);
|
|
523
|
+
|
|
524
|
+
const steadyStart = renderer.inspect();
|
|
525
|
+
const steadyStartFrame = steadyStart.frame.frameId;
|
|
526
|
+
const steadyStartMeshBytes = steadyStart.dynamicGeometry?.meshBytes ?? 0;
|
|
527
|
+
for (let frame = 0; frame < steadyFrames; frame += 1) {
|
|
528
|
+
const phase = (frame / steadyFrames) * Math.PI * 2;
|
|
529
|
+
recipe.setPosition(successorA, [-1.55 + Math.sin(phase) * 0.18, 0.25, -0.25]);
|
|
530
|
+
recipe.setPosition(successorB, [1.55 + Math.cos(phase) * 0.18, -0.15, -0.25]);
|
|
531
|
+
world.update(1 / 60).unwrap();
|
|
532
|
+
const receipt = await submitFrame(world, renderer, lease);
|
|
533
|
+
expect(receipt.frameId).toBe(steadyStartFrame + frame + 1);
|
|
534
|
+
}
|
|
535
|
+
const steadyEnd = renderer.inspect();
|
|
536
|
+
// This loop keeps two ordinary MeshRenderer entities moving while every
|
|
537
|
+
// submit still uses a real FrameReceipt. Stable capacity and candidate
|
|
538
|
+
// bytes are the bounded resource proof; no per-frame screenshot hides it.
|
|
539
|
+
expect(steadyEnd.state).toBe('alive');
|
|
540
|
+
expect(steadyEnd.frame.frameId).toBe(steadyStartFrame + steadyFrames);
|
|
541
|
+
expect(steadyEnd.dynamicGeometry?.meshBytes ?? 0).toBe(steadyStartMeshBytes);
|
|
542
|
+
// biome-ignore lint/suspicious/noConsole: the bounded loop emits its resource proof.
|
|
543
|
+
console.info(
|
|
544
|
+
'[wave1-rendering:p0] steady',
|
|
545
|
+
JSON.stringify({
|
|
546
|
+
frames: steadyFrames,
|
|
547
|
+
start: inspectionStats(steadyStart),
|
|
548
|
+
end: inspectionStats(steadyEnd),
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
const roofOffscreen = movedSuccessors;
|
|
553
|
+
recipe.setPosition(recipe.entities.roof, [0, 2.35, 0.5]);
|
|
554
|
+
world.update(1 / 60).unwrap();
|
|
555
|
+
const roofOccluding = await waitForFrame(
|
|
556
|
+
world,
|
|
557
|
+
renderer,
|
|
558
|
+
lease,
|
|
559
|
+
canvas,
|
|
560
|
+
'roof-occluding',
|
|
561
|
+
true,
|
|
562
|
+
);
|
|
563
|
+
expect(pixelDelta(roofOffscreen.pixels, roofOccluding.pixels).changedPixels).toBeGreaterThan(0);
|
|
564
|
+
recipe.setPosition(recipe.entities.roof, [20, 2.9, 0.5]);
|
|
565
|
+
world.update(1 / 60).unwrap();
|
|
566
|
+
const roofRestored = await waitForFrame(world, renderer, lease, canvas, 'roof-offscreen', true);
|
|
567
|
+
expect(pixelDelta(roofOccluding.pixels, roofRestored.pixels).changedPixels).toBeGreaterThan(0);
|
|
568
|
+
|
|
569
|
+
recipe.destroy(recipe.entities.wall);
|
|
570
|
+
world.update(1 / 60).unwrap();
|
|
571
|
+
const deleted = await waitForFrame(world, renderer, lease, canvas, 'wall-deleted', true);
|
|
572
|
+
expect(pixelDelta(roofRestored.pixels, deleted.pixels).changedPixels).toBeGreaterThan(0);
|
|
573
|
+
renderer.retireDynamicGeometry(apertureCandidate).unwrap();
|
|
574
|
+
|
|
575
|
+
const replacementEntity = recipe.spawnReplacement();
|
|
576
|
+
world.update(1 / 60).unwrap();
|
|
577
|
+
const replacementClosed = await waitForFrame(
|
|
578
|
+
world,
|
|
579
|
+
renderer,
|
|
580
|
+
lease,
|
|
581
|
+
canvas,
|
|
582
|
+
'replacement-created',
|
|
583
|
+
true,
|
|
584
|
+
);
|
|
585
|
+
expect(pixelDelta(deleted.pixels, replacementClosed.pixels).changedPixels).toBeGreaterThan(0);
|
|
586
|
+
const replacementPrepared = prepareDynamic(
|
|
587
|
+
renderer,
|
|
588
|
+
world,
|
|
589
|
+
replacementEntity,
|
|
590
|
+
recipe.assets.wallAperture,
|
|
591
|
+
recipe.assets.wallApertureHandle,
|
|
592
|
+
2,
|
|
593
|
+
);
|
|
594
|
+
const replacementCandidate = acceptDynamic(renderer, world, replacementPrepared);
|
|
595
|
+
const replacementOpened = await waitForFrame(
|
|
596
|
+
world,
|
|
597
|
+
renderer,
|
|
598
|
+
lease,
|
|
599
|
+
canvas,
|
|
600
|
+
'replacement-aperture-published',
|
|
601
|
+
true,
|
|
602
|
+
);
|
|
603
|
+
expect(renderer.dynamicGeometryReceipt(replacementCandidate)?.frameId).toBe(
|
|
604
|
+
replacementOpened.receipt.frameId,
|
|
605
|
+
);
|
|
606
|
+
expect(
|
|
607
|
+
pixelDelta(replacementClosed.pixels, replacementOpened.pixels).changedPixels,
|
|
608
|
+
).toBeGreaterThan(0);
|
|
609
|
+
|
|
610
|
+
world.set(recipe.entities.camera, Camera, { antialias: ANTIALIAS_FXAA }).unwrap();
|
|
611
|
+
const fxaa = await waitForFrame(world, renderer, lease, canvas, 'aa-fxaa', true);
|
|
612
|
+
expect(fxaa.inspection.temporal.mode).toBe('fxaa');
|
|
613
|
+
world.set(recipe.entities.camera, Camera, { antialias: ANTIALIAS_NONE }).unwrap();
|
|
614
|
+
canvas.width = resizedCanvas.width;
|
|
615
|
+
canvas.height = resizedCanvas.height;
|
|
616
|
+
canvas.style.width = `${resizedCanvas.width}px`;
|
|
617
|
+
canvas.style.height = `${resizedCanvas.height}px`;
|
|
618
|
+
world
|
|
619
|
+
.set(recipe.entities.camera, Camera, { aspect: resizedCanvas.width / resizedCanvas.height })
|
|
620
|
+
.unwrap();
|
|
621
|
+
const resized = await waitForFrame(world, renderer, lease, canvas, 'aa-none-resize', true);
|
|
622
|
+
expect(resized.pixels.width).toBeLessThan(fxaa.pixels.width);
|
|
623
|
+
expect(resized.pixels.height).toBeLessThan(fxaa.pixels.height);
|
|
624
|
+
expect(resized.inspection.temporal.mode).toBe('none');
|
|
625
|
+
world.set(recipe.entities.camera, Camera, { antialias: ANTIALIAS_TAA }).unwrap();
|
|
626
|
+
let taa = await waitForFrame(world, renderer, lease, canvas, 'aa-taa-first', true);
|
|
627
|
+
expect(taa.inspection.temporal.mode).toBe('taa');
|
|
628
|
+
for (let frame = 0; frame < 3; frame += 1) {
|
|
629
|
+
world.update(1 / 60).unwrap();
|
|
630
|
+
taa = await waitForFrame(world, renderer, lease, canvas, `aa-taa-${frame + 2}`, false);
|
|
631
|
+
}
|
|
632
|
+
expect(taa.inspection.temporal.historyValid).toBe(true);
|
|
633
|
+
expect(taa.inspection.temporal.coverage).toEqual(resizedCanvas);
|
|
634
|
+
const finalTaa = await waitForFrame(
|
|
635
|
+
world,
|
|
636
|
+
renderer,
|
|
637
|
+
lease,
|
|
638
|
+
canvas,
|
|
639
|
+
'aa-taa-final',
|
|
640
|
+
true,
|
|
641
|
+
'wave1-p0-final-taa',
|
|
642
|
+
);
|
|
643
|
+
expect(finalTaa.inspection.temporal.historyValid).toBe(true);
|
|
644
|
+
|
|
645
|
+
// Backing resolution can change independently of CSS pixels (DPR/render scale).
|
|
646
|
+
canvas.width = resizedCanvas.width * 2;
|
|
647
|
+
canvas.height = resizedCanvas.height * 2;
|
|
648
|
+
const scaled = await waitForFrame(world, renderer, lease, canvas, 'aa-backing-scale', true);
|
|
649
|
+
expect(scaled.pixels.width).toBe(finalTaa.pixels.width);
|
|
650
|
+
expect(scaled.pixels.height).toBe(finalTaa.pixels.height);
|
|
651
|
+
expect(scaled.inspection.temporal.coverage).toEqual({
|
|
652
|
+
width: resizedCanvas.width * 2,
|
|
653
|
+
height: resizedCanvas.height * 2,
|
|
654
|
+
});
|
|
655
|
+
expect(scaled.inspection.temporal.resetReason).toBe('resize');
|
|
656
|
+
canvas.width = resizedCanvas.width;
|
|
657
|
+
canvas.height = resizedCanvas.height;
|
|
658
|
+
await submitFrame(world, renderer, lease);
|
|
659
|
+
|
|
660
|
+
for (const yaw of [0.02, 0.04, 0.08, 0.45, -0.45, 0]) {
|
|
661
|
+
world
|
|
662
|
+
.set(recipe.entities.camera, Transform, {
|
|
663
|
+
quat: [0, Math.sin(yaw / 2), 0, Math.cos(yaw / 2)],
|
|
664
|
+
})
|
|
665
|
+
.unwrap();
|
|
666
|
+
world.update(1 / 60).unwrap();
|
|
667
|
+
await submitFrame(world, renderer, lease);
|
|
668
|
+
}
|
|
669
|
+
for (let frame = 0; frame < 16; frame += 1) await submitFrame(world, renderer, lease);
|
|
670
|
+
const settled = await waitForFrame(
|
|
671
|
+
world,
|
|
672
|
+
renderer,
|
|
673
|
+
lease,
|
|
674
|
+
canvas,
|
|
675
|
+
'aa-after-camera-motion',
|
|
676
|
+
true,
|
|
677
|
+
);
|
|
678
|
+
world.set(recipe.entities.camera, Camera, { antialias: ANTIALIAS_NONE }).unwrap();
|
|
679
|
+
const reference = await waitForFrame(
|
|
680
|
+
world,
|
|
681
|
+
renderer,
|
|
682
|
+
lease,
|
|
683
|
+
canvas,
|
|
684
|
+
'aa-motion-reference',
|
|
685
|
+
false,
|
|
686
|
+
);
|
|
687
|
+
const error =
|
|
688
|
+
pixelDelta(
|
|
689
|
+
cropPixels(settled.pixels, 0.5, 0.55, 0.35, 0.4),
|
|
690
|
+
cropPixels(reference.pixels, 0.5, 0.55, 0.35, 0.4),
|
|
691
|
+
).meanAbsoluteChannelDelta / 255;
|
|
692
|
+
expect(error).toBeLessThanOrEqual(0.05);
|
|
693
|
+
world.set(recipe.entities.camera, Camera, { antialias: ANTIALIAS_TAA }).unwrap();
|
|
694
|
+
await submitFrame(world, renderer, lease);
|
|
695
|
+
|
|
696
|
+
const cameraValues = world.get(recipe.entities.camera, Camera).unwrap();
|
|
697
|
+
const alternate = world
|
|
698
|
+
.spawn(
|
|
699
|
+
{ component: Transform, data: { pos: [0, 0.65, 7] } },
|
|
700
|
+
{ component: Camera, data: { ...cameraValues, clearColor: [...cameraValues.clearColor] } },
|
|
701
|
+
)
|
|
702
|
+
.unwrap();
|
|
703
|
+
try {
|
|
704
|
+
const original = await waitForFrame(
|
|
705
|
+
world,
|
|
706
|
+
renderer,
|
|
707
|
+
lease,
|
|
708
|
+
canvas,
|
|
709
|
+
'aa-camera-original',
|
|
710
|
+
false,
|
|
711
|
+
undefined,
|
|
712
|
+
recipe.entities.camera,
|
|
713
|
+
);
|
|
714
|
+
const switched = await waitForFrame(
|
|
715
|
+
world,
|
|
716
|
+
renderer,
|
|
717
|
+
lease,
|
|
718
|
+
canvas,
|
|
719
|
+
'aa-camera-switched',
|
|
720
|
+
true,
|
|
721
|
+
undefined,
|
|
722
|
+
alternate,
|
|
723
|
+
);
|
|
724
|
+
expect(switched.inspection.temporal.viewIdentity).not.toBe(
|
|
725
|
+
original.inspection.temporal.viewIdentity,
|
|
726
|
+
);
|
|
727
|
+
expect(switched.inspection.temporal.resetReason).toBe('view-switch');
|
|
728
|
+
} finally {
|
|
729
|
+
world.despawn(alternate).unwrap();
|
|
730
|
+
}
|
|
731
|
+
await submitFrame(world, renderer, lease);
|
|
732
|
+
renderer.releaseSurface().unwrap();
|
|
733
|
+
world
|
|
734
|
+
.set(recipe.entities.camera, Camera, { historyVersion: cameraValues.historyVersion + 1 })
|
|
735
|
+
.unwrap();
|
|
736
|
+
renderer.restoreSurface().unwrap();
|
|
737
|
+
const resumed = await waitForFrame(world, renderer, lease, canvas, 'aa-resumed', true);
|
|
738
|
+
expect(resumed.inspection.temporal.resetReason).toBe('history-version');
|
|
739
|
+
expect(resumed.inspection.temporal.historyValid).toBe(true);
|
|
740
|
+
|
|
741
|
+
expect(errors).toEqual([]);
|
|
742
|
+
expect(worldErrors).not.toHaveBeenCalled();
|
|
743
|
+
} finally {
|
|
744
|
+
unsubscribe();
|
|
745
|
+
worldErrors.mockRestore();
|
|
746
|
+
await renderer.dispose();
|
|
747
|
+
recipe.dispose();
|
|
748
|
+
await scene.fiber.dispose();
|
|
749
|
+
canvas.remove();
|
|
750
|
+
}
|
|
751
|
+
}, 120000);
|