@forgeax/engine-runtime 0.1.19 → 0.1.21
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 +102 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts.map +1 -1
- package/dist/__tests__/lights-preservation.fixup.test.d.ts +2 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-dimension-mismatch.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-dimension-mismatch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/texture-dimensions.browser.test.d.ts +2 -0
- package/dist/__tests__/texture-dimensions.browser.test.d.ts.map +1 -0
- package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts +2 -0
- package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts.map +1 -0
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/active-camera.unit.test.ts +6 -8
- package/src/__tests__/asset-registry.recursive.spec.ts +13 -19
- package/src/__tests__/asset.unit.test.ts +28 -25
- package/src/__tests__/basis-catalog-dispatch.integration.test.ts +10 -9
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
- package/src/__tests__/components.test-d.ts +10 -3
- package/src/__tests__/components.unit.test.ts +13 -7
- package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +12 -10
- package/src/__tests__/create-renderer-assembly.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-gpu-pass-timing.unit.test.ts +32 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +66 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +11 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +2 -3
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +2 -3
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +2 -3
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +2 -3
- package/src/__tests__/dawn/upload-texture.dawn.test.ts +4 -6
- package/src/__tests__/derive-bgl-integration.test.ts +11 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +6 -4
- package/src/__tests__/errors.unit.test.ts +71 -20
- package/src/__tests__/extract-frames-merge.test.ts +48 -40
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
- package/src/__tests__/four-path-compression.integration.test.ts +2 -2
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +13 -0
- package/src/__tests__/geometry.unit.test.ts +7 -10
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +2 -3
- package/src/__tests__/helpers/tilemap-assets.ts +3 -4
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +6 -3
- package/src/__tests__/light-snapshot.test-d.ts +8 -0
- package/src/__tests__/lights-preservation.fixup.test.ts +127 -0
- package/src/__tests__/lights.unit.test.ts +75 -2214
- package/src/__tests__/loadbyguid-texture-intern.unit.test.ts +3 -4
- package/src/__tests__/loader-name-survival.test.ts +2 -3
- package/src/__tests__/material-texture-dimension-mismatch.unit.test.ts +18 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +10 -2
- package/src/__tests__/mip-gate.unit.test.ts +11 -8
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +2 -3
- package/src/__tests__/new-kind-refs-contract.test.ts +2 -3
- package/src/__tests__/pbr-pipeline.unit.test.ts +29 -5
- package/src/__tests__/pipeline.unit.test.ts +66 -221
- package/src/__tests__/point-light-shadow.browser.test.ts +208 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +67 -9
- package/src/__tests__/render-error-exhaustive.test-d.ts +92 -9
- package/src/__tests__/render-system-extract.test.ts +4 -6
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +8 -7
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +6 -4
- package/src/__tests__/render-system-skin-bg.unit.test.ts +14 -9
- package/src/__tests__/renderer-recover.unit.test.ts +8 -0
- package/src/__tests__/renderer-surface.unit.test.ts +229 -3
- package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
- package/src/__tests__/shadow-csm-component.test.ts +31 -24
- package/src/__tests__/shadow-csm-extract.test.ts +156 -47
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +72 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +19 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +41 -45
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +140 -58
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +6 -2
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +4 -6
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +4 -2
- package/src/__tests__/ssao-bgl.test.ts +10 -16
- package/src/__tests__/ssao-passes.test.ts +19 -2
- package/src/__tests__/systems.unit.test.ts +59 -33
- package/src/__tests__/texture-dimensions.browser.test.ts +127 -0
- package/src/__tests__/transcode-fallback.unit.test.ts +5 -1
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/__tests__/volumetric-fog-record-recovery.integration.test.ts +180 -0
- package/src/createRenderer.ts +4 -0
- package/src/index.ts +1 -2
|
@@ -10,7 +10,21 @@
|
|
|
10
10
|
// This file is scoped to the 'browser' vitest project (file-naming convention
|
|
11
11
|
// *.browser.test.ts); the 'dawn' and 'unit' projects exclude it.
|
|
12
12
|
|
|
13
|
+
import { HANDLE_CUBE } from '@forgeax/engine-assets-runtime';
|
|
14
|
+
import { World } from '@forgeax/engine-ecs';
|
|
15
|
+
import {
|
|
16
|
+
Camera,
|
|
17
|
+
DirectionalLight,
|
|
18
|
+
Materials,
|
|
19
|
+
MeshFilter,
|
|
20
|
+
MeshRenderer,
|
|
21
|
+
PointLight,
|
|
22
|
+
PointLightShadow,
|
|
23
|
+
} from '@forgeax/engine-render';
|
|
24
|
+
import { propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
13
25
|
import { describe, expect, it } from 'vitest';
|
|
26
|
+
import { page } from 'vitest/browser';
|
|
27
|
+
import { constructRuntimeRendererHost } from '../renderer-host';
|
|
14
28
|
|
|
15
29
|
// WebGPU bitmask constants per spec (avoids needing @webgpu/types globals
|
|
16
30
|
// in the runtime tsconfig). Values are stable across implementations.
|
|
@@ -192,87 +206,198 @@ describe('M0 cube_array comparison sampler (browser)', () => {
|
|
|
192
206
|
});
|
|
193
207
|
});
|
|
194
208
|
|
|
195
|
-
// feat-20260612-point-light-shadows-urp-hdrp
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Round-2 status (feat-20260612 Round-2 commit-B + commit-C): the BGL
|
|
203
|
-
// hookup landed -- POINT_SHADOW_AVAILABLE is registered at all 3
|
|
204
|
-
// vite-plugin-shader define sites, the runtime PBR view BGL emits
|
|
205
|
-
// always-on bindings 5 (cube_array depth atlas) + 6 (shadowParams UBO 64 B),
|
|
206
|
-
// and recordPointShadowPass walks validated entries with real drawIndexed
|
|
207
|
-
// calls + per-face VP write to viewUniformBuffer.lightSpaceMatrix. The
|
|
208
|
-
// shadowParams UBO is written per frame in record-stage with
|
|
209
|
-
// (near, far, 1/(far-near), 0) per shadow-casting snapshot.
|
|
210
|
-
//
|
|
211
|
-
// What remains for this `it.skip` to flip to `it`: the full
|
|
212
|
-
// createRenderer + canvas + readPixels + scene scaffolding (mirror of
|
|
213
|
-
// `light-casters-9-light.browser.test.ts` shape: spawnCamera + spawn
|
|
214
|
-
// occluder cube + spawn ground plane + spawnPointAt + spawnPointShadow
|
|
215
|
-
// + driving FRAMES_PER_SCENE rAF ticks + sampleBlockAverage at occluded
|
|
216
|
-
// vs free sample sites). The wiring is unblocked at the engine layer; the
|
|
217
|
-
// scope of writing the test fixture itself + tuning the lighting setup so
|
|
218
|
-
// occluded < non-occluded reliably under chromium headless presentation
|
|
219
|
-
// timing exceeds Round-2 fix-up budget. Tracked as a sub-followup
|
|
220
|
-
// (feat-followup-point-shadow-browser-readback) per implement-decisions.md.
|
|
221
|
-
describe('URP point shadow pixel readback (browser, T-M3-6 / AC-14)', () => {
|
|
222
|
-
it.skip('occluded fragment darker than non-occluded -- BGL hookup landed in Round-2; full createRenderer + readPixels scaffolding deferred', () => {
|
|
223
|
-
// Pseudocode for the assertion the runnable test will make once the
|
|
224
|
-
// hookup lands:
|
|
225
|
-
//
|
|
226
|
-
// const renderer = createRenderer(canvas, { ... });
|
|
227
|
-
// const world = new World();
|
|
228
|
-
// world.spawn({ component: Transform, data: {...} },
|
|
229
|
-
// { component: PointLight, data: {...} },
|
|
230
|
-
// { component: PointLightShadow, data: {} });
|
|
231
|
-
// // ... ground plane + occluder ...
|
|
232
|
-
// renderer.draw(world);
|
|
233
|
-
// const px = await readback(canvas, occludedX, occludedY);
|
|
234
|
-
// const py = await readback(canvas, freeX, freeY);
|
|
235
|
-
// expect(px.r + px.g + px.b).toBeLessThan(py.r + py.g + py.b);
|
|
236
|
-
// expect(py.r + py.g + py.b).toBeGreaterThan(0); // non-black frame
|
|
237
|
-
});
|
|
238
|
-
});
|
|
209
|
+
// feat-20260612-point-light-shadows-urp-hdrp M5 / AC-24 focused consumer.
|
|
210
|
+
// This is a real Standard createRenderer path rather than a cube-array-only
|
|
211
|
+
// primitive: the same caster/receiver scene is rendered with and without the
|
|
212
|
+
// PointLightShadow component, then the canvas pixels and renderer pass facts
|
|
213
|
+
// are checked together.
|
|
214
|
+
const browserReady = typeof navigator !== 'undefined' && navigator.gpu !== undefined;
|
|
215
|
+
const REAL_CANVAS_SIZE = 96;
|
|
239
216
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
217
|
+
function spawnPointShadowScene(world: World, withShadow: boolean): void {
|
|
218
|
+
const material = world.allocSharedRef(
|
|
219
|
+
'MaterialAsset',
|
|
220
|
+
Materials.standard({
|
|
221
|
+
baseColor: [0.78, 0.26, 0.08, 1],
|
|
222
|
+
metallic: 0,
|
|
223
|
+
roughness: 0.45,
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
world
|
|
227
|
+
.spawn(
|
|
228
|
+
{ component: Transform, data: { pos: [0, 0, 0], scale: [1.3, 1.3, 1.3] } },
|
|
229
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
230
|
+
{ component: MeshRenderer, data: { materials: [material] } },
|
|
231
|
+
)
|
|
232
|
+
.unwrap();
|
|
233
|
+
world
|
|
234
|
+
.spawn(
|
|
235
|
+
{ component: Transform, data: { pos: [0, -1.65, 0], scale: [3.5, 0.15, 3.5] } },
|
|
236
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
237
|
+
{ component: MeshRenderer, data: { materials: [material] } },
|
|
238
|
+
)
|
|
239
|
+
.unwrap();
|
|
240
|
+
world
|
|
241
|
+
.spawn(
|
|
242
|
+
{ component: Transform, data: { pos: [0, 0, 6] } },
|
|
243
|
+
{ component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 50 } },
|
|
244
|
+
)
|
|
245
|
+
.unwrap();
|
|
246
|
+
world
|
|
247
|
+
.spawn({
|
|
248
|
+
component: DirectionalLight,
|
|
249
|
+
data: { direction: [-0.4, -0.8, -1], intensity: 0.35, castShadow: false },
|
|
250
|
+
})
|
|
251
|
+
.unwrap();
|
|
252
|
+
const point = [1.8, 2.6, 2.2] as const;
|
|
253
|
+
world
|
|
254
|
+
.spawn(
|
|
255
|
+
{ component: Transform, data: { pos: point } },
|
|
256
|
+
{ component: PointLight, data: { color: [1, 0.72, 0.4], intensity: 22, range: 12 } },
|
|
257
|
+
...(withShadow
|
|
258
|
+
? [{ component: PointLightShadow, data: { mapSize: 256, nearPlane: 0.1, farPlane: 25 } }]
|
|
259
|
+
: []),
|
|
260
|
+
)
|
|
261
|
+
.unwrap();
|
|
262
|
+
|
|
263
|
+
// The standalone renderer host does not install scenePlugin for this
|
|
264
|
+
// fixture. Publish world-space matrices before extract so the pixel proof
|
|
265
|
+
// exercises the actual caster/receiver scene instead of the clear colour.
|
|
266
|
+
propagateTransforms(world).unwrap();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function waitForPresentedPixels(canvas: HTMLCanvasElement): Promise<Uint8Array> {
|
|
270
|
+
let latest = new Uint8Array(REAL_CANVAS_SIZE * REAL_CANVAS_SIZE * 4);
|
|
271
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
272
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
273
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 25));
|
|
274
|
+
// CDP-backed element screenshots read the compositor surface directly;
|
|
275
|
+
// createImageBitmap(canvas) can race swap-chain presentation in headless
|
|
276
|
+
// Chromium and return an all-zero opaque surface.
|
|
277
|
+
const shot = await page.elementLocator(canvas).screenshot({ base64: true, save: false });
|
|
278
|
+
const b64 = typeof shot === 'string' ? shot : shot.base64;
|
|
279
|
+
const binary = atob(b64);
|
|
280
|
+
const encoded = new Uint8Array(binary.length);
|
|
281
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
282
|
+
encoded[index] = binary.charCodeAt(index);
|
|
283
|
+
}
|
|
284
|
+
const bitmap = await createImageBitmap(new Blob([encoded], { type: 'image/png' }));
|
|
285
|
+
const surface = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
286
|
+
const context = surface.getContext('2d', { willReadFrequently: true });
|
|
287
|
+
if (context === null) {
|
|
288
|
+
bitmap.close();
|
|
289
|
+
throw new Error('point-shadow browser test: canvas readback context unavailable');
|
|
290
|
+
}
|
|
291
|
+
context.drawImage(bitmap, 0, 0);
|
|
292
|
+
const image = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
293
|
+
bitmap.close();
|
|
294
|
+
latest = new Uint8Array(image.data);
|
|
295
|
+
let nonBlack = false;
|
|
296
|
+
for (let index = 0; index < latest.length; index += 4) {
|
|
297
|
+
if ((latest[index] ?? 0) + (latest[index + 1] ?? 0) + (latest[index + 2] ?? 0) > 0) {
|
|
298
|
+
nonBlack = true;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (nonBlack) return latest;
|
|
303
|
+
}
|
|
304
|
+
return latest;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function pixelLuma(pixels: Uint8Array): number {
|
|
308
|
+
let sum = 0;
|
|
309
|
+
for (let index = 0; index < pixels.length; index += 4) {
|
|
310
|
+
sum += (pixels[index] ?? 0) + (pixels[index + 1] ?? 0) + (pixels[index + 2] ?? 0);
|
|
311
|
+
}
|
|
312
|
+
return sum / (pixels.length / 4);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function pixelDifference(a: Uint8Array, b: Uint8Array): number {
|
|
316
|
+
let difference = 0;
|
|
317
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 4) {
|
|
318
|
+
difference += Math.abs((a[index] ?? 0) - (b[index] ?? 0));
|
|
319
|
+
difference += Math.abs((a[index + 1] ?? 0) - (b[index + 1] ?? 0));
|
|
320
|
+
difference += Math.abs((a[index + 2] ?? 0) - (b[index + 2] ?? 0));
|
|
321
|
+
}
|
|
322
|
+
return difference;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
it.skipIf(!browserReady)(
|
|
326
|
+
'Standard point-shadow scene differs from its no-shadow baseline',
|
|
327
|
+
async () => {
|
|
328
|
+
const canvas = document.createElement('canvas');
|
|
329
|
+
canvas.width = REAL_CANVAS_SIZE;
|
|
330
|
+
canvas.height = REAL_CANVAS_SIZE;
|
|
331
|
+
canvas.style.width = `${REAL_CANVAS_SIZE}px`;
|
|
332
|
+
canvas.style.height = `${REAL_CANVAS_SIZE}px`;
|
|
333
|
+
document.body.appendChild(canvas);
|
|
334
|
+
let renderer: import('@forgeax/engine-render').Renderer | undefined;
|
|
335
|
+
try {
|
|
336
|
+
const host = await constructRuntimeRendererHost(
|
|
337
|
+
canvas,
|
|
338
|
+
{},
|
|
339
|
+
{ shaderManifestUrl: '/shaders/manifest.json' },
|
|
340
|
+
);
|
|
341
|
+
if (!host.ok) throw host.error;
|
|
342
|
+
renderer = host.value.renderer;
|
|
343
|
+
const errors: string[] = [];
|
|
344
|
+
const unsubscribe = renderer.subscribe((event) => {
|
|
345
|
+
if (event.kind === 'error') errors.push(event.error.code);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const shadowWorld = new World();
|
|
349
|
+
spawnPointShadowScene(shadowWorld, true);
|
|
350
|
+
const shadowAttachment = renderer.attach(shadowWorld);
|
|
351
|
+
expect(shadowAttachment.ok).toBe(true);
|
|
352
|
+
if (!shadowAttachment.ok) throw shadowAttachment.error;
|
|
353
|
+
for (let frame = 0; frame < 6; frame += 1) {
|
|
354
|
+
shadowWorld.update(1 / 60).unwrap();
|
|
355
|
+
const drawn = renderer.draw({
|
|
356
|
+
leases: [shadowAttachment.value],
|
|
357
|
+
camera: { lease: shadowAttachment.value },
|
|
358
|
+
environment: { lease: shadowAttachment.value },
|
|
359
|
+
});
|
|
360
|
+
expect(drawn.ok).toBe(true);
|
|
361
|
+
if (!drawn.ok) throw drawn.error;
|
|
362
|
+
}
|
|
363
|
+
const shadowPixels = await waitForPresentedPixels(canvas);
|
|
364
|
+
const shadowInspection = renderer.inspect();
|
|
365
|
+
expect(shadowInspection.pointShadow?.status).toBe('ready');
|
|
366
|
+
expect(shadowInspection.pointShadow?.requested).toBe(1);
|
|
367
|
+
expect(shadowInspection.pointShadow?.admitted).toBe(1);
|
|
368
|
+
expect(
|
|
369
|
+
shadowInspection.perFramePassNames.filter((name) => name.startsWith('point-shadow-')),
|
|
370
|
+
).toHaveLength(6);
|
|
371
|
+
|
|
372
|
+
const noShadowWorld = new World();
|
|
373
|
+
spawnPointShadowScene(noShadowWorld, false);
|
|
374
|
+
const noShadowAttachment = renderer.attach(noShadowWorld);
|
|
375
|
+
expect(noShadowAttachment.ok).toBe(true);
|
|
376
|
+
if (!noShadowAttachment.ok) throw noShadowAttachment.error;
|
|
377
|
+
for (let frame = 0; frame < 6; frame += 1) {
|
|
378
|
+
noShadowWorld.update(1 / 60).unwrap();
|
|
379
|
+
const drawn = renderer.draw({
|
|
380
|
+
leases: [noShadowAttachment.value],
|
|
381
|
+
camera: { lease: noShadowAttachment.value },
|
|
382
|
+
environment: { lease: noShadowAttachment.value },
|
|
383
|
+
});
|
|
384
|
+
expect(drawn.ok).toBe(true);
|
|
385
|
+
if (!drawn.ok) throw drawn.error;
|
|
386
|
+
}
|
|
387
|
+
const noShadowPixels = await waitForPresentedPixels(canvas);
|
|
388
|
+
const noShadowInspection = renderer.inspect();
|
|
389
|
+
expect(noShadowInspection.pointShadow?.status).toBe('inactive');
|
|
390
|
+
expect(
|
|
391
|
+
noShadowInspection.perFramePassNames.filter((name) => name.startsWith('point-shadow-')),
|
|
392
|
+
).toHaveLength(0);
|
|
393
|
+
expect(pixelLuma(shadowPixels)).toBeGreaterThan(0);
|
|
394
|
+
expect(pixelLuma(noShadowPixels)).toBeGreaterThan(0);
|
|
395
|
+
expect(pixelDifference(shadowPixels, noShadowPixels)).toBeGreaterThan(0);
|
|
396
|
+
expect(errors).toEqual([]);
|
|
397
|
+
unsubscribe();
|
|
398
|
+
} finally {
|
|
399
|
+
if (renderer !== undefined) await renderer.dispose();
|
|
400
|
+
canvas.remove();
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
);
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
BYTES_PER_LIGHT_SLOT,
|
|
23
23
|
LIGHTSLOT_LAYOUT,
|
|
24
24
|
packLightSlot,
|
|
25
|
+
SPOT_LIGHT_PROJECTOR_FLAG,
|
|
25
26
|
} from '../../../render/src/light-buffer-layout';
|
|
26
27
|
import { buildPointShadowMatrices } from '../../../render/src/render-system-extract';
|
|
27
28
|
import {
|
|
@@ -164,8 +165,8 @@ function spawnValidatedPointShadow(
|
|
|
164
165
|
expect(r.error).toBeInstanceOf(ShadowInvalidConfigError);
|
|
165
166
|
const detail = (r.error as unknown as ShadowInvalidConfigError).detail;
|
|
166
167
|
expect(detail.field).toBe('mapSize');
|
|
167
|
-
expect(detail.
|
|
168
|
-
expect(detail.
|
|
168
|
+
expect(detail.actual).toBe(0);
|
|
169
|
+
expect(detail.bound).toEqual({ kind: 'lower-bound', operator: '>=', value: 1 });
|
|
169
170
|
// .expected and .hint carry actionable strings
|
|
170
171
|
expect((r.error as unknown as ShadowInvalidConfigError).expected).toContain('mapSize');
|
|
171
172
|
expect((r.error as unknown as ShadowInvalidConfigError).hint).toContain('mapSize');
|
|
@@ -190,8 +191,8 @@ function spawnValidatedPointShadow(
|
|
|
190
191
|
expect(r.error).toBeInstanceOf(ShadowInvalidConfigError);
|
|
191
192
|
const detail = (r.error as unknown as ShadowInvalidConfigError).detail;
|
|
192
193
|
expect(detail.field).toBe('farPlane');
|
|
193
|
-
expect(detail.
|
|
194
|
-
expect(detail.
|
|
194
|
+
expect(detail.actual).toBe(0.5);
|
|
195
|
+
expect(detail.bound).toEqual({ kind: 'lower-bound', operator: '>=', value: 1.0 });
|
|
195
196
|
}
|
|
196
197
|
});
|
|
197
198
|
|
|
@@ -577,7 +578,7 @@ function spawnValidatedPointShadow(
|
|
|
577
578
|
expect(withShadow[15]).toBe(25);
|
|
578
579
|
});
|
|
579
580
|
|
|
580
|
-
it('AC-13: spot light packLightSlot
|
|
581
|
+
it('AC-13: spot light packLightSlot writes its shadow defaults into the tail lanes', () => {
|
|
581
582
|
const spotSnap = {
|
|
582
583
|
kind: 'spot' as const,
|
|
583
584
|
entity: 0,
|
|
@@ -595,8 +596,9 @@ function spawnValidatedPointShadow(
|
|
|
595
596
|
farPlane: 50,
|
|
596
597
|
shadowAtlasTile: -1,
|
|
597
598
|
};
|
|
598
|
-
//
|
|
599
|
-
//
|
|
599
|
+
// SpotLight uses the former pad lanes for the shadow intensity and
|
|
600
|
+
// PCF-kernel defaults. The atlas tile remains the -1 sentinel while
|
|
601
|
+
// projector/shadow consumers read the appended payload fields.
|
|
600
602
|
const packed = packLightSlot(spotSnap, {
|
|
601
603
|
shadowAtlasLayer: 0,
|
|
602
604
|
near: 0.1,
|
|
@@ -604,8 +606,64 @@ function spawnValidatedPointShadow(
|
|
|
604
606
|
});
|
|
605
607
|
const i32 = new Int32Array(packed.buffer);
|
|
606
608
|
expect(i32[13]).toBe(-1); // sentinel; spot lights stay shadowless
|
|
607
|
-
expect(packed[14]).
|
|
608
|
-
expect(packed[15]).
|
|
609
|
+
expect(packed[14]).toBeCloseTo(1, 5);
|
|
610
|
+
expect(packed[15]).toBeCloseTo(3, 5);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('keeps authored spot shadow intensity and PCF width in the cluster lanes', () => {
|
|
614
|
+
const spotSnap = {
|
|
615
|
+
kind: 'spot' as const,
|
|
616
|
+
entity: 0,
|
|
617
|
+
position: vec3.create(0, 0, 0),
|
|
618
|
+
direction: vec3.create(0, -1, 0),
|
|
619
|
+
color: vec3.create(1, 1, 1),
|
|
620
|
+
intensity: 1,
|
|
621
|
+
invRangeSquared: 0,
|
|
622
|
+
cosInner: 0.95,
|
|
623
|
+
cosOuter: 0.85,
|
|
624
|
+
castShadow: true,
|
|
625
|
+
lightViewProj: undefined,
|
|
626
|
+
mapSize: 2048,
|
|
627
|
+
nearPlane: 0.1,
|
|
628
|
+
farPlane: 50,
|
|
629
|
+
shadowAtlasTile: 2,
|
|
630
|
+
shadowIntensity: 0.35,
|
|
631
|
+
pcfKernelSize: 5,
|
|
632
|
+
};
|
|
633
|
+
const packed = packLightSlot(spotSnap);
|
|
634
|
+
const i32 = new Int32Array(packed.buffer);
|
|
635
|
+
expect(i32[13]).toBe(2);
|
|
636
|
+
expect(packed[14]).toBeCloseTo(0.35, 5);
|
|
637
|
+
expect(packed[15]).toBeCloseTo(5, 5);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('marks the accepted projector without losing a shadow tile', () => {
|
|
641
|
+
const spotSnap = {
|
|
642
|
+
kind: 'spot' as const,
|
|
643
|
+
entity: 7,
|
|
644
|
+
position: vec3.create(0, 2, 0),
|
|
645
|
+
direction: vec3.create(0, -1, 0),
|
|
646
|
+
color: vec3.create(1, 1, 1),
|
|
647
|
+
intensity: 1,
|
|
648
|
+
invRangeSquared: 0.04,
|
|
649
|
+
cosInner: 0.95,
|
|
650
|
+
cosOuter: 0.85,
|
|
651
|
+
castShadow: true,
|
|
652
|
+
lightViewProj: new Float32Array(16),
|
|
653
|
+
mapSize: 512,
|
|
654
|
+
nearPlane: 0.1,
|
|
655
|
+
farPlane: 25,
|
|
656
|
+
shadowAtlasTile: 2,
|
|
657
|
+
projectorAsset: { kind: 'texture' } as never,
|
|
658
|
+
};
|
|
659
|
+
const packed = packLightSlot(spotSnap, undefined, true);
|
|
660
|
+
const encoded = new Int32Array(packed.buffer)[13] ?? 0;
|
|
661
|
+
expect(encoded & SPOT_LIGHT_PROJECTOR_FLAG).toBe(SPOT_LIGHT_PROJECTOR_FLAG);
|
|
662
|
+
expect(encoded & (SPOT_LIGHT_PROJECTOR_FLAG - 1)).toBe(2);
|
|
663
|
+
|
|
664
|
+
const projectorOnly = packLightSlot({ ...spotSnap, shadowAtlasTile: -1 }, undefined, true);
|
|
665
|
+
const projectorOnlyEncoded = new Int32Array(projectorOnly.buffer)[13] ?? 0;
|
|
666
|
+
expect(projectorOnlyEncoded).toBe(SPOT_LIGHT_PROJECTOR_FLAG);
|
|
609
667
|
});
|
|
610
668
|
|
|
611
669
|
it('AC-05: LIGHTSLOT_LAYOUT byteSize unchanged at 64 (no-regression for standard)', () => {
|
|
@@ -40,6 +40,8 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
40
40
|
return code;
|
|
41
41
|
case 'taa-unavailable':
|
|
42
42
|
return code;
|
|
43
|
+
case 'standard-profile-invalid':
|
|
44
|
+
return code;
|
|
43
45
|
case 'frame-receipt-stale':
|
|
44
46
|
return code;
|
|
45
47
|
case 'renderer-contract-failed':
|
|
@@ -64,11 +66,23 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
64
66
|
return code;
|
|
65
67
|
case 'equirect-projection-failed':
|
|
66
68
|
return code;
|
|
67
|
-
case '
|
|
69
|
+
case 'standard-light-budget-exceeded':
|
|
70
|
+
return code;
|
|
71
|
+
case 'standard-cluster-index-overflow':
|
|
72
|
+
return code;
|
|
73
|
+
case 'standard-cluster-transport-unavailable':
|
|
74
|
+
return code;
|
|
75
|
+
case 'render-target-descriptor-invalid':
|
|
76
|
+
return code;
|
|
77
|
+
case 'render-target-capability-missing':
|
|
78
|
+
return code;
|
|
79
|
+
case 'render-target-state-invalid':
|
|
80
|
+
return code;
|
|
81
|
+
case 'render-target-operation-failed':
|
|
68
82
|
return code;
|
|
69
|
-
case '
|
|
83
|
+
case 'reflection-probe-budget-exceeded':
|
|
70
84
|
return code;
|
|
71
|
-
case '
|
|
85
|
+
case 'render-intent-invalid':
|
|
72
86
|
return code;
|
|
73
87
|
case 'point-shadow-atlas-uninitialized':
|
|
74
88
|
return code;
|
|
@@ -114,6 +128,16 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
114
128
|
return code;
|
|
115
129
|
case 'points-lines-prepare-failed':
|
|
116
130
|
return code;
|
|
131
|
+
case 'projector-binding-failed':
|
|
132
|
+
return code;
|
|
133
|
+
case 'volume-owner-conflict':
|
|
134
|
+
return code;
|
|
135
|
+
case 'volume-density-shape-mismatch':
|
|
136
|
+
return code;
|
|
137
|
+
case 'volume-invalid-bounds':
|
|
138
|
+
return code;
|
|
139
|
+
case 'volume-invalid-parameters':
|
|
140
|
+
return code;
|
|
117
141
|
default: {
|
|
118
142
|
const exhaustive: never = code;
|
|
119
143
|
return exhaustive;
|
|
@@ -176,6 +200,10 @@ function narrowRenderError(err: RenderError): void {
|
|
|
176
200
|
void err.detail.operation;
|
|
177
201
|
void err.detail.causes;
|
|
178
202
|
break;
|
|
203
|
+
case 'standard-profile-invalid':
|
|
204
|
+
void err.detail.field;
|
|
205
|
+
void err.detail.actual;
|
|
206
|
+
break;
|
|
179
207
|
case 'frame-receipt-stale':
|
|
180
208
|
void err.detail.frameId;
|
|
181
209
|
void err.detail.receiptGeneration;
|
|
@@ -200,7 +228,9 @@ function narrowRenderError(err: RenderError): void {
|
|
|
200
228
|
break;
|
|
201
229
|
case 'shadow-invalid-config':
|
|
202
230
|
void err.detail.field; // string
|
|
203
|
-
void err.detail.
|
|
231
|
+
void err.detail.actual; // number
|
|
232
|
+
void err.detail.bound;
|
|
233
|
+
void err.detail.reason; // string
|
|
204
234
|
break;
|
|
205
235
|
case 'environment-source-conflict':
|
|
206
236
|
void err.detail.owners;
|
|
@@ -227,17 +257,51 @@ function narrowRenderError(err: RenderError): void {
|
|
|
227
257
|
case 'equirect-projection-failed':
|
|
228
258
|
void err.detail.handle; // number
|
|
229
259
|
break;
|
|
230
|
-
case '
|
|
260
|
+
case 'standard-light-budget-exceeded':
|
|
231
261
|
void err.detail.actual; // number
|
|
232
262
|
void err.detail.budget; // number
|
|
233
263
|
break;
|
|
234
|
-
case '
|
|
264
|
+
case 'standard-cluster-index-overflow':
|
|
235
265
|
void err.detail.actual; // number
|
|
236
266
|
void err.detail.capacity; // number
|
|
237
267
|
break;
|
|
238
|
-
case '
|
|
239
|
-
void err.detail.
|
|
240
|
-
void err.detail.
|
|
268
|
+
case 'standard-cluster-transport-unavailable':
|
|
269
|
+
void err.detail.requested; // number
|
|
270
|
+
void err.detail.admitted; // 0
|
|
271
|
+
break;
|
|
272
|
+
case 'render-target-descriptor-invalid':
|
|
273
|
+
void err.detail.field;
|
|
274
|
+
void err.detail.value;
|
|
275
|
+
void err.detail.expected;
|
|
276
|
+
break;
|
|
277
|
+
case 'render-target-capability-missing':
|
|
278
|
+
void err.detail.operation;
|
|
279
|
+
void err.detail.requested;
|
|
280
|
+
void err.detail.capability;
|
|
281
|
+
void err.detail.actual;
|
|
282
|
+
break;
|
|
283
|
+
case 'render-target-state-invalid':
|
|
284
|
+
void err.detail.operation;
|
|
285
|
+
void err.detail.reason;
|
|
286
|
+
void err.detail.state;
|
|
287
|
+
void err.detail.generation;
|
|
288
|
+
break;
|
|
289
|
+
case 'render-target-operation-failed':
|
|
290
|
+
void err.detail.operation;
|
|
291
|
+
void err.detail.stage;
|
|
292
|
+
void err.detail.generation;
|
|
293
|
+
void err.detail.cause;
|
|
294
|
+
void err.detail.recovery;
|
|
295
|
+
break;
|
|
296
|
+
case 'reflection-probe-budget-exceeded':
|
|
297
|
+
void err.detail.actual;
|
|
298
|
+
void err.detail.budget;
|
|
299
|
+
break;
|
|
300
|
+
case 'render-intent-invalid':
|
|
301
|
+
void err.detail.component;
|
|
302
|
+
void err.detail.field;
|
|
303
|
+
void err.detail.value;
|
|
304
|
+
void err.detail.allowed;
|
|
241
305
|
break;
|
|
242
306
|
case 'point-shadow-atlas-uninitialized':
|
|
243
307
|
// No detail on this class.
|
|
@@ -333,6 +397,25 @@ function narrowRenderError(err: RenderError): void {
|
|
|
333
397
|
void err.detail.capability;
|
|
334
398
|
void err.detail.stage;
|
|
335
399
|
break;
|
|
400
|
+
case 'projector-binding-failed':
|
|
401
|
+
void err.detail.guid;
|
|
402
|
+
void err.detail.status;
|
|
403
|
+
break;
|
|
404
|
+
case 'volume-owner-conflict':
|
|
405
|
+
void err.detail.ownerCount;
|
|
406
|
+
break;
|
|
407
|
+
case 'volume-density-shape-mismatch':
|
|
408
|
+
void err.detail.guid;
|
|
409
|
+
void err.detail.viewDimension;
|
|
410
|
+
break;
|
|
411
|
+
case 'volume-invalid-bounds':
|
|
412
|
+
void err.detail.min;
|
|
413
|
+
void err.detail.max;
|
|
414
|
+
break;
|
|
415
|
+
case 'volume-invalid-parameters':
|
|
416
|
+
void err.detail.field;
|
|
417
|
+
void err.detail.value;
|
|
418
|
+
break;
|
|
336
419
|
default: {
|
|
337
420
|
const exhaustive: never = err;
|
|
338
421
|
void exhaustive;
|
|
@@ -1193,12 +1193,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1193
1193
|
'TextureAsset',
|
|
1194
1194
|
{
|
|
1195
1195
|
kind: 'texture',
|
|
1196
|
-
width: 1,
|
|
1197
|
-
height: 1,
|
|
1196
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
1198
1197
|
format: 'rgba8unorm',
|
|
1199
1198
|
data: new Uint8Array([255, 255, 255, 255]),
|
|
1200
1199
|
colorSpace: 'linear',
|
|
1201
|
-
|
|
1200
|
+
mips: { kind: 'none' },
|
|
1202
1201
|
},
|
|
1203
1202
|
);
|
|
1204
1203
|
}
|
|
@@ -1211,12 +1210,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1211
1210
|
const textureGuid = AssetGuid.random();
|
|
1212
1211
|
assets.catalog(textureGuid, {
|
|
1213
1212
|
kind: 'texture',
|
|
1214
|
-
width: 1,
|
|
1215
|
-
height: 1,
|
|
1213
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
1216
1214
|
format: 'rgba8unorm',
|
|
1217
1215
|
data: new Uint8Array([255, 255, 255, 255]),
|
|
1218
1216
|
colorSpace: 'linear',
|
|
1219
|
-
|
|
1217
|
+
mips: { kind: 'none' },
|
|
1220
1218
|
} satisfies TextureAsset);
|
|
1221
1219
|
const matHandle = world.allocSharedRef<'MaterialAsset', MaterialAsset>('MaterialAsset', {
|
|
1222
1220
|
kind: 'material',
|