@forgeax/engine-runtime 0.1.19 → 0.1.20
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/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/helpers/tilemap-assets.d.ts.map +1 -1
- 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.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/asset-registry.recursive.spec.ts +12 -18
- package/src/__tests__/asset.unit.test.ts +28 -25
- package/src/__tests__/basis-catalog-dispatch.integration.test.ts +10 -9
- package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +12 -10
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +2 -3
- 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 +20 -0
- package/src/__tests__/four-path-compression.integration.test.ts +2 -2
- 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 +4 -2
- package/src/__tests__/lights.unit.test.ts +38 -17
- 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 +23 -4
- package/src/__tests__/pipeline.unit.test.ts +18 -19
- package/src/__tests__/point-light-shadow.unit.test.ts +6 -5
- package/src/__tests__/render-error-exhaustive.test-d.ts +29 -0
- package/src/__tests__/render-system-extract.test.ts +4 -6
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +4 -6
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +2 -3
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +56 -39
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +3 -1
- 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__/systems.unit.test.ts +15 -2
- package/src/__tests__/texture-dimensions.browser.test.ts +127 -0
- package/src/__tests__/transcode-fallback.unit.test.ts +5 -1
- package/src/__tests__/volumetric-fog-record-recovery.integration.test.ts +180 -0
- package/src/createRenderer.ts +3 -0
|
@@ -1295,11 +1295,12 @@ function spawnValidatedLight(
|
|
|
1295
1295
|
});
|
|
1296
1296
|
});
|
|
1297
1297
|
|
|
1298
|
-
describe('packSpotLight - std430
|
|
1298
|
+
describe('packSpotLight - std430 80B layout (feat-20260625 M2 w6)', () => {
|
|
1299
1299
|
// feat-20260625-spot-light-shadow-mapping M2 w6 (D-4): SpotLight std430
|
|
1300
|
-
// stride
|
|
1301
|
-
// slots 12..14
|
|
1302
|
-
// (sentinel -1 = unassigned/clipped)
|
|
1300
|
+
// stride 64 -> 80 (16 -> 20 floats). Slots 0..11 keep the prior layout;
|
|
1301
|
+
// slots 12..14 carry receiver controls, slot 15 is shadowAtlasTile i32
|
|
1302
|
+
// (sentinel -1 = unassigned/clipped), slot 16 carries shadowIntensity,
|
|
1303
|
+
// and slots 17..19 are the std430 tail pad.
|
|
1303
1304
|
function makeSnap(shadowAtlasTile: number): SpotLightSnapshot {
|
|
1304
1305
|
return {
|
|
1305
1306
|
kind: 'spot',
|
|
@@ -1319,11 +1320,11 @@ function spawnValidatedLight(
|
|
|
1319
1320
|
};
|
|
1320
1321
|
}
|
|
1321
1322
|
|
|
1322
|
-
it('emits
|
|
1323
|
+
it('emits 20 floats / 80 bytes; slots 0..11 unchanged from the 48B layout', () => {
|
|
1323
1324
|
const out = packSpotLight(makeSnap(0));
|
|
1324
1325
|
expect(out).toBeInstanceOf(Float32Array);
|
|
1325
|
-
expect(out.length).toBe(
|
|
1326
|
-
expect(out.byteLength).toBe(
|
|
1326
|
+
expect(out.length).toBe(20);
|
|
1327
|
+
expect(out.byteLength).toBe(80);
|
|
1327
1328
|
// Slot 0..2: position vec3.
|
|
1328
1329
|
expect(out[0]).toBeCloseTo(3.0, 6);
|
|
1329
1330
|
expect(out[1]).toBeCloseTo(4.0, 6);
|
|
@@ -1342,10 +1343,16 @@ function spawnValidatedLight(
|
|
|
1342
1343
|
expect(out[10]).toBeCloseTo(0.0, 6);
|
|
1343
1344
|
// Slot 11: cosOuter (packed into direction.w lane).
|
|
1344
1345
|
expect(out[11]).toBeCloseTo(0.866, 6);
|
|
1345
|
-
// Slot 12..14:
|
|
1346
|
-
expect(out[12]).
|
|
1347
|
-
expect(out[13]).
|
|
1348
|
-
expect(out[14]).toBe(
|
|
1346
|
+
// Slot 12..14: shared receiver controls.
|
|
1347
|
+
expect(out[12]).toBeCloseTo(0.005, 6);
|
|
1348
|
+
expect(out[13]).toBeCloseTo(0.05, 6);
|
|
1349
|
+
expect(out[14]).toBe(3);
|
|
1350
|
+
// Slot 16 carries the authored shadow visibility factor; the tail is
|
|
1351
|
+
// explicit zero padding for the 80-byte std430 array stride.
|
|
1352
|
+
expect(out[16]).toBe(1);
|
|
1353
|
+
expect(out[17]).toBe(0);
|
|
1354
|
+
expect(out[18]).toBe(0);
|
|
1355
|
+
expect(out[19]).toBe(0);
|
|
1349
1356
|
});
|
|
1350
1357
|
|
|
1351
1358
|
it('slot 15 carries shadowAtlasTile as i32 (tile=0)', () => {
|
|
@@ -2308,13 +2315,20 @@ function spawnValidatedLight(
|
|
|
2308
2315
|
});
|
|
2309
2316
|
});
|
|
2310
2317
|
|
|
2311
|
-
describe('sampler reuse (binding 4)
|
|
2312
|
-
it('keeps binding 4 as the
|
|
2318
|
+
describe('sampler reuse (binding 4) plus optional projector filtering sampler', () => {
|
|
2319
|
+
it('keeps binding 4 as the comparison sampler and binding 12 as the projector filtering sampler', () => {
|
|
2313
2320
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2314
2321
|
const samplers = entries.filter((e) => e.sampler !== undefined);
|
|
2315
|
-
expect(samplers).toHaveLength(
|
|
2316
|
-
expect(samplers
|
|
2317
|
-
expect(samplers
|
|
2322
|
+
expect(samplers).toHaveLength(2);
|
|
2323
|
+
expect(samplers.find((entry) => entry.binding === 4)?.sampler?.type).toBe('comparison');
|
|
2324
|
+
expect(samplers.find((entry) => entry.binding === 12)?.sampler?.type).toBe('filtering');
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2327
|
+
it('omits the projector filtering sampler on the 16-texture capability path', () => {
|
|
2328
|
+
const entries = buildPbrViewBglEntries({ storageBuffer: true, projectorAvailable: false });
|
|
2329
|
+
expect(entries.find((entry) => entry.binding === 11)).toBeUndefined();
|
|
2330
|
+
expect(entries.find((entry) => entry.binding === 12)).toBeUndefined();
|
|
2331
|
+
expect(entries.find((entry) => entry.binding === 4)?.sampler?.type).toBe('comparison');
|
|
2318
2332
|
});
|
|
2319
2333
|
});
|
|
2320
2334
|
|
|
@@ -2353,10 +2367,17 @@ function spawnValidatedLight(
|
|
|
2353
2367
|
});
|
|
2354
2368
|
|
|
2355
2369
|
describe('full binding roster reserves 9 for Points/Lines at 10', () => {
|
|
2356
|
-
it('exposes bindings 0..8 plus
|
|
2370
|
+
it('exposes bindings 0..8 plus optional projector 11+12 and binding 10', () => {
|
|
2357
2371
|
const bindings = buildPbrViewBglEntries({ storageBuffer: true })
|
|
2358
2372
|
.map((e) => e.binding)
|
|
2359
2373
|
.sort((a, b) => a - b);
|
|
2374
|
+
expect(bindings).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12]);
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
it('drops only optional projector bindings on the low sampled-texture path', () => {
|
|
2378
|
+
const bindings = buildPbrViewBglEntries({ storageBuffer: true, projectorAvailable: false })
|
|
2379
|
+
.map((e) => e.binding)
|
|
2380
|
+
.sort((a, b) => a - b);
|
|
2360
2381
|
expect(bindings).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 10]);
|
|
2361
2382
|
});
|
|
2362
2383
|
});
|
|
@@ -64,13 +64,12 @@ function registerQuadMesh(world: World): Handle<'MeshAsset', 'shared'> {
|
|
|
64
64
|
function textureAsset(red = 255): TextureAsset {
|
|
65
65
|
return {
|
|
66
66
|
kind: 'texture',
|
|
67
|
-
width: 1,
|
|
68
|
-
height: 1,
|
|
67
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
69
68
|
format: 'rgba8unorm',
|
|
70
69
|
data: new Uint8Array([red, 255, 255, 255]),
|
|
71
70
|
colorSpace: 'linear',
|
|
72
|
-
|
|
73
|
-
}
|
|
71
|
+
mips: { kind: 'none' },
|
|
72
|
+
};
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
// Build a loadByGuid-shaped scene: the material's baseColorTexture paramValue is
|
|
@@ -77,11 +77,10 @@ function makeSharedPack(): unknown {
|
|
|
77
77
|
|
|
78
78
|
const TEX_METADATA = {
|
|
79
79
|
kind: 'texture',
|
|
80
|
-
width: 1,
|
|
81
|
-
height: 1,
|
|
80
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
82
81
|
format: 'rgba8unorm',
|
|
83
82
|
colorSpace: 'srgb',
|
|
84
|
-
|
|
83
|
+
mips: { kind: 'none' },
|
|
85
84
|
};
|
|
86
85
|
|
|
87
86
|
// `includeNames=false` is the FALSIFY variant (entry.name stripped).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { derive } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('material texture dimension mismatch contract', () => {
|
|
5
|
+
it('carries the authored array view dimension into the material binding interface', () => {
|
|
6
|
+
const derived = derive([{ name: 'layers', type: 'texture2d_array' }]);
|
|
7
|
+
|
|
8
|
+
expect(derived.coordinateRecords[0]?.parameter).toBe('layers');
|
|
9
|
+
expect(derived.bglEntries[2]?.texture?.viewDimension).toBe('2d-array');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('carries the authored volume view dimension without changing the submit boundary', () => {
|
|
13
|
+
const derived = derive([{ name: 'volume', type: 'texture3d' }]);
|
|
14
|
+
|
|
15
|
+
expect(derived.bglEntries[2]?.texture?.viewDimension).toBe('3d');
|
|
16
|
+
expect(derived.resourceBindings).toHaveLength(2);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
|
|
11
11
|
describe('material texture UV scale [w37]', () => {
|
|
12
12
|
it('maps a non-aligned BC7 logical edge below padded physical storage', () => {
|
|
13
|
-
const scale = materialTextureUvScale({
|
|
13
|
+
const scale = materialTextureUvScale({
|
|
14
|
+
shape: { viewDimension: '2d', extent: { width: 2085, height: 1573 } },
|
|
15
|
+
format: 'bc7-rgba-unorm',
|
|
16
|
+
});
|
|
14
17
|
expect(scale).toEqual([2085 / 2088, 1573 / 1576]);
|
|
15
18
|
expect(scale[0]).toBeLessThan(1);
|
|
16
19
|
expect(scale[1]).toBeLessThan(1);
|
|
@@ -18,7 +21,12 @@ describe('material texture UV scale [w37]', () => {
|
|
|
18
21
|
|
|
19
22
|
it('uses identity scale for uncompressed and fallback texture bindings', () => {
|
|
20
23
|
expect(materialTextureUvScale(undefined)).toEqual([1, 1]);
|
|
21
|
-
expect(
|
|
24
|
+
expect(
|
|
25
|
+
materialTextureUvScale({
|
|
26
|
+
shape: { viewDimension: '2d', extent: { width: 17, height: 9 } },
|
|
27
|
+
format: 'rgba8unorm',
|
|
28
|
+
}),
|
|
29
|
+
).toEqual([1, 1]);
|
|
22
30
|
});
|
|
23
31
|
|
|
24
32
|
it('keeps compatibility param schemas compact when they have no textures', () => {
|
|
@@ -27,19 +27,20 @@ import { deriveRenderDataTexture } from '../../../render/src/render-data';
|
|
|
27
27
|
function tex(overrides: Partial<TextureAsset>): TextureAsset {
|
|
28
28
|
return {
|
|
29
29
|
kind: 'texture',
|
|
30
|
-
width: 8,
|
|
31
|
-
height: 8,
|
|
30
|
+
shape: { viewDimension: '2d', extent: { width: 8, height: 8 } },
|
|
32
31
|
format: 'rgba8unorm',
|
|
33
32
|
data: new Uint8Array(8 * 8 * 4),
|
|
34
33
|
colorSpace: 'linear',
|
|
35
|
-
|
|
34
|
+
mips: { kind: 'none' },
|
|
36
35
|
...overrides,
|
|
37
36
|
};
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
describe('deriveRenderDataTexture -- mip gate on compressed formats (w30, AC-09)', () => {
|
|
41
40
|
it('compressed format + mipmap:true (runtime mip-gen requested) -> error', () => {
|
|
42
|
-
const res = deriveRenderDataTexture(
|
|
41
|
+
const res = deriveRenderDataTexture(
|
|
42
|
+
tex({ format: 'bc7-rgba-unorm', mips: { kind: 'generate' } }),
|
|
43
|
+
);
|
|
43
44
|
expect(res.ok).toBe(false);
|
|
44
45
|
if (!res.ok) {
|
|
45
46
|
expect(res.error.code).toBe('mipgen-unsupported-compressed-format');
|
|
@@ -50,7 +51,9 @@ describe('deriveRenderDataTexture -- mip gate on compressed formats (w30, AC-09)
|
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
it('mip-gate error fires for an ETC2 compressed format too', () => {
|
|
53
|
-
const res = deriveRenderDataTexture(
|
|
54
|
+
const res = deriveRenderDataTexture(
|
|
55
|
+
tex({ format: 'etc2-rgba8unorm', mips: { kind: 'generate' } }),
|
|
56
|
+
);
|
|
54
57
|
expect(res.ok).toBe(false);
|
|
55
58
|
if (!res.ok) expect(res.error.code).toBe('mipgen-unsupported-compressed-format');
|
|
56
59
|
});
|
|
@@ -59,20 +62,20 @@ describe('deriveRenderDataTexture -- mip gate on compressed formats (w30, AC-09)
|
|
|
59
62
|
// The KTX2 loader baked the mip chain offline; this is the normal
|
|
60
63
|
// compressed-upload path and must NOT trip the runtime-mip-gen gate.
|
|
61
64
|
const res = deriveRenderDataTexture(
|
|
62
|
-
tex({ format: 'bc7-rgba-unorm',
|
|
65
|
+
tex({ format: 'bc7-rgba-unorm', mips: { kind: 'packed', levelCount: 4 } }),
|
|
63
66
|
);
|
|
64
67
|
expect(res.ok).toBe(true);
|
|
65
68
|
if (res.ok) expect(res.value.mipLevelCount).toBe(4);
|
|
66
69
|
});
|
|
67
70
|
|
|
68
71
|
it('compressed format + mipmap:false -> ok (single level)', () => {
|
|
69
|
-
const res = deriveRenderDataTexture(tex({ format: 'bc7-rgba-unorm',
|
|
72
|
+
const res = deriveRenderDataTexture(tex({ format: 'bc7-rgba-unorm', mips: { kind: 'none' } }));
|
|
70
73
|
expect(res.ok).toBe(true);
|
|
71
74
|
if (res.ok) expect(res.value.mipLevelCount).toBe(1);
|
|
72
75
|
});
|
|
73
76
|
|
|
74
77
|
it('uncompressed format + mipmap:true -> ok (existing runtime mip-gen path unchanged)', () => {
|
|
75
|
-
const res = deriveRenderDataTexture(tex({ format: 'rgba8unorm',
|
|
78
|
+
const res = deriveRenderDataTexture(tex({ format: 'rgba8unorm', mips: { kind: 'generate' } }));
|
|
76
79
|
expect(res.ok).toBe(true);
|
|
77
80
|
if (res.ok) expect(res.value.mipLevelCount).toBeGreaterThan(1);
|
|
78
81
|
});
|
|
@@ -260,12 +260,11 @@ describe('feat-20260604-msaa M2 w9 [F-1]: LDR sprite + MSAA split sub-pass cover
|
|
|
260
260
|
const synth = buildSyntheticRgba();
|
|
261
261
|
const synthPod = {
|
|
262
262
|
kind: 'texture' as const,
|
|
263
|
-
width: synth.width,
|
|
264
|
-
height: synth.height,
|
|
263
|
+
shape: { viewDimension: '2d' as const, extent: { width: synth.width, height: synth.height } },
|
|
265
264
|
format: 'rgba8unorm-srgb' as const,
|
|
266
265
|
data: synth.data,
|
|
267
266
|
colorSpace: 'srgb' as const,
|
|
268
|
-
|
|
267
|
+
mips: { kind: 'none' as const },
|
|
269
268
|
};
|
|
270
269
|
const TEX_GUID = '00000000-0000-7000-8000-0000000005a1';
|
|
271
270
|
const SAMPLER_GUID = '00000000-0000-7000-8000-0000000005a2';
|
|
@@ -62,12 +62,11 @@ describe('AC-07 — a new reference appears in the load graph via envelope.refs'
|
|
|
62
62
|
// new reference field whose GUID it dropped into refs[]".
|
|
63
63
|
reg.catalog(parseGuid(EXTRA_REF_GUID), {
|
|
64
64
|
kind: 'texture',
|
|
65
|
-
width: 1,
|
|
66
|
-
height: 1,
|
|
65
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
67
66
|
format: 'rgba8unorm',
|
|
68
67
|
data: new Uint8Array(4),
|
|
69
68
|
colorSpace: 'srgb',
|
|
70
|
-
|
|
69
|
+
mips: { kind: 'none' },
|
|
71
70
|
});
|
|
72
71
|
|
|
73
72
|
const packIndex = [
|
|
@@ -44,7 +44,7 @@ function makeSpec(): PipelineSpec {
|
|
|
44
44
|
|
|
45
45
|
describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', () => {
|
|
46
46
|
describe('pbr-view', () => {
|
|
47
|
-
it('storageBuffer=true:
|
|
47
|
+
it('storageBuffer=true: 12 entries with read-only-storage on bindings 1+2 and projector bindings 11+12', () => {
|
|
48
48
|
const spec = makeSpec();
|
|
49
49
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
50
50
|
kind: 'pbr-view',
|
|
@@ -60,11 +60,15 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
60
60
|
// Points/Lines viewport UBO occupies binding 10, while w25 (scope-amend
|
|
61
61
|
// webkit-fallback) folded the former binding 9 spotLightViewProj matrices
|
|
62
62
|
// UBO into the View UBO (`view.spotLightViewProj`), so the BGL stays at 9
|
|
63
|
-
// entries (bindings 0..8 plus
|
|
64
|
-
// buffer count <= 11
|
|
65
|
-
|
|
63
|
+
// entries (bindings 0..8 plus optional projector 11+12 and binding 10)
|
|
64
|
+
// — keeping the WebGL2 fallback fragment uniform-buffer count <= 11
|
|
65
|
+
// (GLES 3.0), while the projector remains available on the default
|
|
66
|
+
// WebGPU path.
|
|
67
|
+
expect(out.entries.length).toBe(12);
|
|
66
68
|
expect(out.entries.find((entry) => entry.binding === 9)).toBeUndefined();
|
|
67
69
|
expect(out.entries.find((entry) => entry.binding === 10)?.buffer?.type).toBe('uniform');
|
|
70
|
+
expect(out.entries.find((entry) => entry.binding === 11)?.texture?.viewDimension).toBe('2d');
|
|
71
|
+
expect(out.entries.find((entry) => entry.binding === 12)?.sampler?.type).toBe('filtering');
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
it('storageBuffer=false: bindings 1+2 fall back to uniform', () => {
|
|
@@ -79,6 +83,21 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
79
83
|
};
|
|
80
84
|
expect(out).toEqual(expected);
|
|
81
85
|
});
|
|
86
|
+
|
|
87
|
+
it('projectorAvailable=false: omits optional projector bindings for the 16-texture limit', () => {
|
|
88
|
+
const spec = makeSpec();
|
|
89
|
+
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
90
|
+
kind: 'pbr-view',
|
|
91
|
+
caps: { storageBuffer: true, projectorAvailable: false },
|
|
92
|
+
});
|
|
93
|
+
expect(out).toEqual({
|
|
94
|
+
label: 'pbr-view-bgl',
|
|
95
|
+
entries: buildPbrViewBglEntries({ storageBuffer: true, projectorAvailable: false }),
|
|
96
|
+
});
|
|
97
|
+
expect(out.entries.find((entry) => entry.binding === 11)).toBeUndefined();
|
|
98
|
+
expect(out.entries.find((entry) => entry.binding === 12)).toBeUndefined();
|
|
99
|
+
expect(out.entries.find((entry) => entry.binding === 10)?.buffer?.type).toBe('uniform');
|
|
100
|
+
});
|
|
82
101
|
});
|
|
83
102
|
|
|
84
103
|
describe('pbr-material-merged', () => {
|
|
@@ -4466,12 +4466,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4466
4466
|
function texturePod(mipmap: boolean, format: GPUTextureFormat = 'rgba8unorm-srgb'): TextureAsset {
|
|
4467
4467
|
return {
|
|
4468
4468
|
kind: 'texture',
|
|
4469
|
-
width: 2,
|
|
4470
|
-
height: 2,
|
|
4469
|
+
shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
|
|
4471
4470
|
format,
|
|
4472
4471
|
data: new Uint8Array(2 * 2 * 4).fill(188),
|
|
4473
4472
|
colorSpace: format.endsWith('-srgb') ? 'srgb' : 'linear',
|
|
4474
|
-
mipmap,
|
|
4473
|
+
mips: { kind: mipmap ? 'generate' : 'none' },
|
|
4475
4474
|
};
|
|
4476
4475
|
}
|
|
4477
4476
|
|
|
@@ -4549,12 +4548,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4549
4548
|
): TextureAsset {
|
|
4550
4549
|
return {
|
|
4551
4550
|
kind: 'texture',
|
|
4552
|
-
width,
|
|
4553
|
-
height,
|
|
4551
|
+
shape: { viewDimension: '2d', extent: { width, height } },
|
|
4554
4552
|
format,
|
|
4555
4553
|
data: new Uint8Array(width * height * 4).fill(170),
|
|
4556
4554
|
colorSpace: format.endsWith('-srgb') ? 'srgb' : 'linear',
|
|
4557
|
-
mipmap,
|
|
4555
|
+
mips: { kind: mipmap ? 'generate' : 'none' },
|
|
4558
4556
|
};
|
|
4559
4557
|
}
|
|
4560
4558
|
|
|
@@ -4566,12 +4564,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4566
4564
|
): TextureAsset {
|
|
4567
4565
|
return {
|
|
4568
4566
|
kind: 'texture',
|
|
4569
|
-
width,
|
|
4570
|
-
height,
|
|
4567
|
+
shape: { viewDimension: '2d', extent: { width, height } },
|
|
4571
4568
|
format,
|
|
4572
4569
|
data: new Uint8Array(width * height * 8),
|
|
4573
4570
|
colorSpace,
|
|
4574
|
-
|
|
4571
|
+
mips: { kind: 'none' },
|
|
4575
4572
|
};
|
|
4576
4573
|
}
|
|
4577
4574
|
|
|
@@ -4658,33 +4655,35 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4658
4655
|
const source = texturePod(false, 'rgba16float');
|
|
4659
4656
|
const res = deriveRenderDataTexture({
|
|
4660
4657
|
...source,
|
|
4661
|
-
data: new Uint8Array(source.width * source.height * 8),
|
|
4658
|
+
data: new Uint8Array(source.shape.extent.width * source.shape.extent.height * 8),
|
|
4662
4659
|
});
|
|
4663
4660
|
expect(res.ok).toBe(true);
|
|
4664
4661
|
if (!res.ok) return;
|
|
4665
|
-
expect(res.value.bytesPerRow).toBe(source.width * 8);
|
|
4662
|
+
expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 8);
|
|
4666
4663
|
});
|
|
4667
4664
|
|
|
4668
4665
|
it('derives row pitch from format when an rgba8 decoder retains trailing bytes', () => {
|
|
4669
4666
|
const source = texturePod(false, 'rgba8unorm');
|
|
4670
4667
|
const res = deriveRenderDataTexture({
|
|
4671
4668
|
...source,
|
|
4672
|
-
data: new Uint8Array(
|
|
4669
|
+
data: new Uint8Array(
|
|
4670
|
+
source.shape.extent.width * source.shape.extent.height * 4 + source.shape.extent.width,
|
|
4671
|
+
),
|
|
4673
4672
|
});
|
|
4674
4673
|
expect(res.ok).toBe(true);
|
|
4675
4674
|
if (!res.ok) return;
|
|
4676
|
-
expect(res.value.bytesPerRow).toBe(source.width * 4);
|
|
4675
|
+
expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 4);
|
|
4677
4676
|
});
|
|
4678
4677
|
|
|
4679
4678
|
it('derives the row pitch from an rgba32float source payload', () => {
|
|
4680
4679
|
const source = texturePod(false, 'rgba32float');
|
|
4681
4680
|
const res = deriveRenderDataTexture({
|
|
4682
4681
|
...source,
|
|
4683
|
-
data: new Uint8Array(source.width * source.height * 16),
|
|
4682
|
+
data: new Uint8Array(source.shape.extent.width * source.shape.extent.height * 16),
|
|
4684
4683
|
});
|
|
4685
4684
|
expect(res.ok).toBe(true);
|
|
4686
4685
|
if (!res.ok) return;
|
|
4687
|
-
expect(res.value.bytesPerRow).toBe(source.width * 16);
|
|
4686
|
+
expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 16);
|
|
4688
4687
|
});
|
|
4689
4688
|
|
|
4690
4689
|
it('rejects an uncompressed payload shorter than its base mip', () => {
|
|
@@ -5091,14 +5090,14 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5091
5090
|
expect(LIGHT_ARRAY_MAX_SLOTS).toBe(4);
|
|
5092
5091
|
});
|
|
5093
5092
|
|
|
5094
|
-
it('POINT_LIGHT_STD430_BYTES and SPOT_LIGHT_STD430_BYTES
|
|
5093
|
+
it('POINT_LIGHT_STD430_BYTES and SPOT_LIGHT_STD430_BYTES match the current URP payloads', async () => {
|
|
5095
5094
|
const { POINT_LIGHT_STD430_BYTES, SPOT_LIGHT_STD430_BYTES } = await import(
|
|
5096
5095
|
'../../../render/src/light-buffer-layout'
|
|
5097
5096
|
);
|
|
5098
5097
|
expect(POINT_LIGHT_STD430_BYTES).toBe(32);
|
|
5099
|
-
//
|
|
5100
|
-
//
|
|
5101
|
-
expect(SPOT_LIGHT_STD430_BYTES).toBe(
|
|
5098
|
+
// SpotLight's projector receiver controls and shadow intensity occupy
|
|
5099
|
+
// the appended fifth vec4 column; point-light layout remains unchanged.
|
|
5100
|
+
expect(SPOT_LIGHT_STD430_BYTES).toBe(80);
|
|
5102
5101
|
});
|
|
5103
5102
|
});
|
|
5104
5103
|
}
|
|
@@ -577,7 +577,7 @@ function spawnValidatedPointShadow(
|
|
|
577
577
|
expect(withShadow[15]).toBe(25);
|
|
578
578
|
});
|
|
579
579
|
|
|
580
|
-
it('AC-13: spot light packLightSlot
|
|
580
|
+
it('AC-13: spot light packLightSlot writes its shadow defaults into the tail lanes', () => {
|
|
581
581
|
const spotSnap = {
|
|
582
582
|
kind: 'spot' as const,
|
|
583
583
|
entity: 0,
|
|
@@ -595,8 +595,9 @@ function spawnValidatedPointShadow(
|
|
|
595
595
|
farPlane: 50,
|
|
596
596
|
shadowAtlasTile: -1,
|
|
597
597
|
};
|
|
598
|
-
//
|
|
599
|
-
//
|
|
598
|
+
// SpotLight uses the former pad lanes for the shadow intensity and
|
|
599
|
+
// PCF-kernel defaults. The atlas tile remains the -1 sentinel while
|
|
600
|
+
// projector/shadow consumers read the appended payload fields.
|
|
600
601
|
const packed = packLightSlot(spotSnap, {
|
|
601
602
|
shadowAtlasLayer: 0,
|
|
602
603
|
near: 0.1,
|
|
@@ -604,8 +605,8 @@ function spawnValidatedPointShadow(
|
|
|
604
605
|
});
|
|
605
606
|
const i32 = new Int32Array(packed.buffer);
|
|
606
607
|
expect(i32[13]).toBe(-1); // sentinel; spot lights stay shadowless
|
|
607
|
-
expect(packed[14]).
|
|
608
|
-
expect(packed[15]).
|
|
608
|
+
expect(packed[14]).toBeCloseTo(1, 5);
|
|
609
|
+
expect(packed[15]).toBeCloseTo(3, 5);
|
|
609
610
|
});
|
|
610
611
|
|
|
611
612
|
it('AC-05: LIGHTSLOT_LAYOUT byteSize unchanged at 64 (no-regression for standard)', () => {
|
|
@@ -114,6 +114,16 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
114
114
|
return code;
|
|
115
115
|
case 'points-lines-prepare-failed':
|
|
116
116
|
return code;
|
|
117
|
+
case 'projector-binding-failed':
|
|
118
|
+
return code;
|
|
119
|
+
case 'volume-owner-conflict':
|
|
120
|
+
return code;
|
|
121
|
+
case 'volume-density-shape-mismatch':
|
|
122
|
+
return code;
|
|
123
|
+
case 'volume-invalid-bounds':
|
|
124
|
+
return code;
|
|
125
|
+
case 'volume-invalid-parameters':
|
|
126
|
+
return code;
|
|
117
127
|
default: {
|
|
118
128
|
const exhaustive: never = code;
|
|
119
129
|
return exhaustive;
|
|
@@ -333,6 +343,25 @@ function narrowRenderError(err: RenderError): void {
|
|
|
333
343
|
void err.detail.capability;
|
|
334
344
|
void err.detail.stage;
|
|
335
345
|
break;
|
|
346
|
+
case 'projector-binding-failed':
|
|
347
|
+
void err.detail.guid;
|
|
348
|
+
void err.detail.status;
|
|
349
|
+
break;
|
|
350
|
+
case 'volume-owner-conflict':
|
|
351
|
+
void err.detail.ownerCount;
|
|
352
|
+
break;
|
|
353
|
+
case 'volume-density-shape-mismatch':
|
|
354
|
+
void err.detail.guid;
|
|
355
|
+
void err.detail.viewDimension;
|
|
356
|
+
break;
|
|
357
|
+
case 'volume-invalid-bounds':
|
|
358
|
+
void err.detail.min;
|
|
359
|
+
void err.detail.max;
|
|
360
|
+
break;
|
|
361
|
+
case 'volume-invalid-parameters':
|
|
362
|
+
void err.detail.field;
|
|
363
|
+
void err.detail.value;
|
|
364
|
+
break;
|
|
336
365
|
default: {
|
|
337
366
|
const exhaustive: never = err;
|
|
338
367
|
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',
|
|
@@ -323,8 +323,7 @@ function makeChequerTexture(): TextureAsset {
|
|
|
323
323
|
// textureView sentinel.
|
|
324
324
|
return {
|
|
325
325
|
kind: 'texture',
|
|
326
|
-
width: 2,
|
|
327
|
-
height: 2,
|
|
326
|
+
shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
|
|
328
327
|
// colorSpace=linear+rgba8unorm to satisfy the runtime's
|
|
329
328
|
// srgb-format consistency validator (layer 7c-1: srgb requires
|
|
330
329
|
// rgba8unorm-srgb). The test only cares that each texture's
|
|
@@ -332,7 +331,7 @@ function makeChequerTexture(): TextureAsset {
|
|
|
332
331
|
format: 'rgba8unorm',
|
|
333
332
|
data: new Uint8Array(2 * 2 * 4),
|
|
334
333
|
colorSpace: 'linear',
|
|
335
|
-
|
|
334
|
+
mips: { kind: 'none' },
|
|
336
335
|
} as unknown as TextureAsset;
|
|
337
336
|
}
|
|
338
337
|
|
|
@@ -586,12 +585,11 @@ async function spawnPbrMissingTextureScene(): Promise<unknown> {
|
|
|
586
585
|
// existing debug-pink fallback.
|
|
587
586
|
const unresidentTexture = {
|
|
588
587
|
kind: 'texture',
|
|
589
|
-
width: 2,
|
|
590
|
-
height: 2,
|
|
588
|
+
shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
|
|
591
589
|
format: 'rgba8unorm-srgb',
|
|
592
590
|
data: new Uint8Array(2 * 2 * 4),
|
|
593
591
|
colorSpace: 'srgb',
|
|
594
|
-
|
|
592
|
+
mips: { kind: 'none' },
|
|
595
593
|
} as unknown as TextureAsset;
|
|
596
594
|
const badTexHandle = world.allocSharedRef('TextureAsset', unresidentTexture) as unknown as Handle<
|
|
597
595
|
'TextureAsset',
|
|
@@ -256,12 +256,11 @@ function cameraTransform() {
|
|
|
256
256
|
function makeTex(): TextureAsset {
|
|
257
257
|
return {
|
|
258
258
|
kind: 'texture',
|
|
259
|
-
width: 2,
|
|
260
|
-
height: 2,
|
|
259
|
+
shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
|
|
261
260
|
format: 'rgba8unorm',
|
|
262
261
|
data: new Uint8Array(2 * 2 * 4),
|
|
263
262
|
colorSpace: 'linear',
|
|
264
|
-
|
|
263
|
+
mips: { kind: 'none' },
|
|
265
264
|
} as unknown as TextureAsset;
|
|
266
265
|
}
|
|
267
266
|
|