@forgeax/engine-runtime 0.1.7 → 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/README.md +45 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/helpers/standard-material-manifest.d.ts +12 -0
- package/dist/__tests__/helpers/standard-material-manifest.d.ts.map +1 -0
- 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-environment-consumer.integration.test.d.ts +2 -0
- package/dist/__tests__/render-environment-consumer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts.map +1 -1
- package/dist/__tests__/renderer-host-fallback.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-host-fallback.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skinned-shadow-mixed.dawn.test.d.ts +2 -0
- package/dist/__tests__/skinned-shadow-mixed.dawn.test.d.ts.map +1 -0
- 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/backend-selection.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.mjs +21 -1
- package/dist/index.mjs.map +1 -1
- package/dist/renderer-host.d.ts +2 -1
- package/dist/renderer-host.d.ts.map +1 -1
- package/dist/renderer-host.mjs +21 -2
- package/dist/renderer-host.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/instances-per-instance-pbr.dawn.test.ts +12 -11
- 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 +56 -0
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +4 -5
- package/src/__tests__/four-path-compression.integration.test.ts +2 -2
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +461 -18
- package/src/__tests__/geometry.unit.test.ts +3 -1
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +2 -3
- package/src/__tests__/helpers/standard-material-manifest.ts +45 -0
- 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 +18 -16
- package/src/__tests__/materials.unit.test.ts +2 -0
- 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 +31 -12
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +6 -4
- package/src/__tests__/pipeline.unit.test.ts +23 -21
- package/src/__tests__/point-light-shadow.unit.test.ts +6 -5
- package/src/__tests__/render-environment-consumer.integration.test.ts +33 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +86 -0
- package/src/__tests__/render-feature-prepared-graphics.fixture.ts +10 -0
- package/src/__tests__/render-system-extract.test.ts +4 -6
- package/src/__tests__/render-system-mega.test.ts +3 -1
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +7 -7
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +5 -4
- package/src/__tests__/render-system-record.test.ts +8 -6
- package/src/__tests__/renderer-host-fallback.unit.test.ts +112 -0
- package/src/__tests__/renderer-lifecycle.integration.test.ts +10 -0
- package/src/__tests__/renderer-surface.unit.test.ts +66 -0
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +5 -1
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +5 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +56 -39
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +385 -0
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +4 -6
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -9
- package/src/__tests__/ssao-passes.test.ts +28 -12
- package/src/__tests__/systems.unit.test.ts +63 -33
- 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/backend-selection.ts +4 -0
- package/src/createRenderer.ts +3 -0
- package/src/renderer-host.ts +30 -3
|
@@ -117,7 +117,7 @@ describe('w7 -- light GPU pack byte-neutral (AC-11)', () => {
|
|
|
117
117
|
expect(out[6]).toBeCloseTo(0.6, 6);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it('packSpotLight writes color slots 4..6 + direction slots 8..10 byte-for-byte (
|
|
120
|
+
it('packSpotLight writes color slots 4..6 + direction slots 8..10 byte-for-byte (80 B)', () => {
|
|
121
121
|
const snap: SpotLightSnapshot = {
|
|
122
122
|
kind: 'spot',
|
|
123
123
|
position: vec3.create(0, 5, 0),
|
|
@@ -135,7 +135,9 @@ describe('w7 -- light GPU pack byte-neutral (AC-11)', () => {
|
|
|
135
135
|
shadowAtlasTile: -1,
|
|
136
136
|
};
|
|
137
137
|
const out = packSpotLight(snap);
|
|
138
|
-
|
|
138
|
+
// SpotLight carries projector receiver controls and the shadow intensity
|
|
139
|
+
// tail in the current std430 payload.
|
|
140
|
+
expect(out.byteLength).toBe(80);
|
|
139
141
|
expect(out[4]).toBeCloseTo(0.3, 6);
|
|
140
142
|
expect(out[5]).toBeCloseTo(0.4, 6);
|
|
141
143
|
expect(out[6]).toBeCloseTo(0.5, 6);
|
|
@@ -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
|
+
});
|
|
@@ -3,7 +3,6 @@ import { vec3 } from '@forgeax/engine-math';
|
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import {
|
|
5
5
|
applyMaterialTextureUvScales,
|
|
6
|
-
BUILTIN_USER_REGION_TEXTURE_FIELDS,
|
|
7
6
|
buildPbrMaterialUboPayload,
|
|
8
7
|
materialTextureUvScale,
|
|
9
8
|
userRegionTextureFieldOrder,
|
|
@@ -11,7 +10,10 @@ import {
|
|
|
11
10
|
|
|
12
11
|
describe('material texture UV scale [w37]', () => {
|
|
13
12
|
it('maps a non-aligned BC7 logical edge below padded physical storage', () => {
|
|
14
|
-
const scale = materialTextureUvScale({
|
|
13
|
+
const scale = materialTextureUvScale({
|
|
14
|
+
shape: { viewDimension: '2d', extent: { width: 2085, height: 1573 } },
|
|
15
|
+
format: 'bc7-rgba-unorm',
|
|
16
|
+
});
|
|
15
17
|
expect(scale).toEqual([2085 / 2088, 1573 / 1576]);
|
|
16
18
|
expect(scale[0]).toBeLessThan(1);
|
|
17
19
|
expect(scale[1]).toBeLessThan(1);
|
|
@@ -19,14 +21,17 @@ describe('material texture UV scale [w37]', () => {
|
|
|
19
21
|
|
|
20
22
|
it('uses identity scale for uncompressed and fallback texture bindings', () => {
|
|
21
23
|
expect(materialTextureUvScale(undefined)).toEqual([1, 1]);
|
|
22
|
-
expect(
|
|
24
|
+
expect(
|
|
25
|
+
materialTextureUvScale({
|
|
26
|
+
shape: { viewDimension: '2d', extent: { width: 17, height: 9 } },
|
|
27
|
+
format: 'rgba8unorm',
|
|
28
|
+
}),
|
|
29
|
+
).toEqual([1, 1]);
|
|
23
30
|
});
|
|
24
31
|
|
|
25
|
-
it('keeps
|
|
26
|
-
expect(userRegionTextureFieldOrder([])).toEqual(
|
|
27
|
-
expect(userRegionTextureFieldOrder([{ name: 'baseColor', type: 'color' }])).toEqual(
|
|
28
|
-
BUILTIN_USER_REGION_TEXTURE_FIELDS,
|
|
29
|
-
);
|
|
32
|
+
it('keeps compatibility param schemas compact when they have no textures', () => {
|
|
33
|
+
expect(userRegionTextureFieldOrder([])).toEqual([]);
|
|
34
|
+
expect(userRegionTextureFieldOrder([{ name: 'baseColor', type: 'color' }])).toEqual([]);
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
it('writes each slot set and transform into the builtin PBR payload', () => {
|
|
@@ -61,8 +66,8 @@ describe('material texture UV scale [w37]', () => {
|
|
|
61
66
|
new World(),
|
|
62
67
|
);
|
|
63
68
|
const f32 = new Float32Array(payload.buffer, payload.byteOffset, payload.byteLength / 4);
|
|
64
|
-
expect(Array.from(f32.slice(
|
|
65
|
-
expect(Array.from(f32.slice(
|
|
69
|
+
expect(Array.from(f32.slice(32, 40))).toEqual([0.125, 0.25, 2, 3, 1, 0.5, 1, 1]);
|
|
70
|
+
expect(Array.from(f32.slice(48, 56))).toEqual([0.75, 0.25, 4, 5, 0, 0, 1, 1]);
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
it('keeps identity UV records for metadata-free standard and builtin schema materials', () => {
|
|
@@ -85,14 +90,11 @@ describe('material texture UV scale [w37]', () => {
|
|
|
85
90
|
expect(
|
|
86
91
|
Array.from(
|
|
87
92
|
new Float32Array(standard.buffer, standard.byteOffset, standard.byteLength / 4).slice(
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
32,
|
|
94
|
+
96,
|
|
90
95
|
),
|
|
91
96
|
),
|
|
92
|
-
).toEqual([
|
|
93
|
-
0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1,
|
|
94
|
-
1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1,
|
|
95
|
-
]);
|
|
97
|
+
).toEqual(Array.from({ length: 8 }, () => [0, 0, 1, 1, 0, 0, 1, 1]).flat());
|
|
96
98
|
|
|
97
99
|
const unlit = buildPbrMaterialUboPayload({
|
|
98
100
|
baseColor: vec3.create(1, 1, 1),
|
|
@@ -197,6 +197,8 @@ describe('Materials.standard multi-pass (w14)', () => {
|
|
|
197
197
|
{ name: 'specularTintTexture', type: 'texture', optional: true },
|
|
198
198
|
{ name: 'emissiveTexture', type: 'texture', optional: true },
|
|
199
199
|
{ name: 'occlusionTexture', type: 'texture', optional: true },
|
|
200
|
+
{ name: 'transmissionTexture', type: 'texture', optional: true },
|
|
201
|
+
{ name: 'thicknessTexture', type: 'texture', optional: true },
|
|
200
202
|
]);
|
|
201
203
|
});
|
|
202
204
|
|
|
@@ -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,25 +83,40 @@ 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', () => {
|
|
85
|
-
it('
|
|
104
|
+
it('26 entries: user-region 17 (derived) + ibl 7 + transmission 2', () => {
|
|
86
105
|
const spec = makeSpec();
|
|
87
106
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
88
107
|
kind: 'pbr-material-merged',
|
|
89
108
|
});
|
|
90
109
|
// Post-M2 (D-1): user-region comes from derive(paramSchema).bglEntries
|
|
91
|
-
// (built-in standard-PBR
|
|
92
|
-
// appended at start = userRegion.length.
|
|
110
|
+
// (built-in standard-PBR 8-texture contract), then IBL + transmission
|
|
111
|
+
// are appended at start = userRegion.length.
|
|
93
112
|
const userRegion = buildPbrMaterialUserRegionEntries();
|
|
94
113
|
const afterIbl = [...userRegion, ...appendInjection(userRegion, 'ibl')];
|
|
95
114
|
const expected = {
|
|
96
115
|
label: 'pbr-material-skylight-bgl',
|
|
97
|
-
entries: [...afterIbl, ...appendInjection(afterIbl, '
|
|
116
|
+
entries: [...afterIbl, ...appendInjection(afterIbl, 'transmission')],
|
|
98
117
|
};
|
|
99
118
|
expect(out).toEqual(expected);
|
|
100
|
-
expect(out.entries.length).toBe(
|
|
119
|
+
expect(out.entries.length).toBe(26);
|
|
101
120
|
});
|
|
102
121
|
});
|
|
103
122
|
|
|
@@ -142,7 +161,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
142
161
|
entries: [
|
|
143
162
|
{
|
|
144
163
|
binding: 0,
|
|
145
|
-
visibility:
|
|
164
|
+
visibility: 0x3,
|
|
146
165
|
buffer: { type: 'read-only-storage', hasDynamicOffset: false },
|
|
147
166
|
},
|
|
148
167
|
],
|
|
@@ -185,7 +204,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
185
204
|
});
|
|
186
205
|
|
|
187
206
|
describe('unlit-material', () => {
|
|
188
|
-
it('
|
|
207
|
+
it('17 entries: base PBR material only (no skylight injection)', () => {
|
|
189
208
|
const spec = makeSpec();
|
|
190
209
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
191
210
|
kind: 'unlit-material',
|
|
@@ -194,7 +213,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
194
213
|
label: 'unlit-material-bgl',
|
|
195
214
|
entries: buildPbrMaterialUserRegionEntries(),
|
|
196
215
|
});
|
|
197
|
-
expect(out.entries.length).toBe(
|
|
216
|
+
expect(out.entries.length).toBe(17);
|
|
198
217
|
});
|
|
199
218
|
});
|
|
200
219
|
});
|
|
@@ -127,14 +127,16 @@ describe('cacheKeyOf passKind dimension', () => {
|
|
|
127
127
|
describe('PassKind open string + KNOWN_PASS_KINDS (feat-20260615 D-10)', () => {
|
|
128
128
|
// M2-T4 expanded KNOWN_PASS_KINDS from 4 to 6 entries by adding 'post-process'
|
|
129
129
|
// and 'skybox'; point-shadow-caster is the engine's depth-only point-light
|
|
130
|
-
// pass and is part of the discoverable catalogue as well.
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
// pass and is part of the discoverable catalogue as well. M3-T4 adds the
|
|
131
|
+
// Standard temporal producer to the shipped pass catalogue.
|
|
132
|
+
it('KNOWN_PASS_KINDS has exactly 8 entries', () => {
|
|
133
|
+
expect(KNOWN_PASS_KINDS).toHaveLength(8);
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
it('KNOWN_PASS_KINDS contains the
|
|
136
|
+
it('KNOWN_PASS_KINDS contains the 8 engine-shipped pass kinds', () => {
|
|
136
137
|
expect(KNOWN_PASS_KINDS).toContain('forward');
|
|
137
138
|
expect(KNOWN_PASS_KINDS).toContain('deferred');
|
|
139
|
+
expect(KNOWN_PASS_KINDS).toContain('temporal');
|
|
138
140
|
expect(KNOWN_PASS_KINDS).toContain('lighting');
|
|
139
141
|
expect(KNOWN_PASS_KINDS).toContain('shadow-caster');
|
|
140
142
|
expect(KNOWN_PASS_KINDS).toContain('point-shadow-caster');
|
|
@@ -138,6 +138,7 @@ import {
|
|
|
138
138
|
import { extractFrame, prepareExtractContext } from '../../../render/src/render-system-extract';
|
|
139
139
|
import { matchPass } from '../../../render/src/systems/pass-selector';
|
|
140
140
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
141
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
141
142
|
|
|
142
143
|
function makeExplicitNullRhi(spies: {
|
|
143
144
|
setIndexBuffer: ReturnType<typeof vi.fn>;
|
|
@@ -2407,7 +2408,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2407
2408
|
sourcePath: `${identifier}.wgsl`,
|
|
2408
2409
|
composedWgsl: '/* stub */',
|
|
2409
2410
|
paramSchema: '[]',
|
|
2410
|
-
variants:
|
|
2411
|
+
variants:
|
|
2412
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
2411
2413
|
});
|
|
2412
2414
|
const manifest = {
|
|
2413
2415
|
schemaVersion: '1.0.0',
|
|
@@ -2697,7 +2699,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2697
2699
|
sourcePath: `${identifier}.wgsl`,
|
|
2698
2700
|
composedWgsl: '/* stub */',
|
|
2699
2701
|
paramSchema: '[]',
|
|
2700
|
-
variants:
|
|
2702
|
+
variants:
|
|
2703
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
2701
2704
|
});
|
|
2702
2705
|
const manifest = {
|
|
2703
2706
|
schemaVersion: '1.0.0',
|
|
@@ -4463,12 +4466,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4463
4466
|
function texturePod(mipmap: boolean, format: GPUTextureFormat = 'rgba8unorm-srgb'): TextureAsset {
|
|
4464
4467
|
return {
|
|
4465
4468
|
kind: 'texture',
|
|
4466
|
-
width: 2,
|
|
4467
|
-
height: 2,
|
|
4469
|
+
shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
|
|
4468
4470
|
format,
|
|
4469
4471
|
data: new Uint8Array(2 * 2 * 4).fill(188),
|
|
4470
4472
|
colorSpace: format.endsWith('-srgb') ? 'srgb' : 'linear',
|
|
4471
|
-
mipmap,
|
|
4473
|
+
mips: { kind: mipmap ? 'generate' : 'none' },
|
|
4472
4474
|
};
|
|
4473
4475
|
}
|
|
4474
4476
|
|
|
@@ -4546,12 +4548,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4546
4548
|
): TextureAsset {
|
|
4547
4549
|
return {
|
|
4548
4550
|
kind: 'texture',
|
|
4549
|
-
width,
|
|
4550
|
-
height,
|
|
4551
|
+
shape: { viewDimension: '2d', extent: { width, height } },
|
|
4551
4552
|
format,
|
|
4552
4553
|
data: new Uint8Array(width * height * 4).fill(170),
|
|
4553
4554
|
colorSpace: format.endsWith('-srgb') ? 'srgb' : 'linear',
|
|
4554
|
-
mipmap,
|
|
4555
|
+
mips: { kind: mipmap ? 'generate' : 'none' },
|
|
4555
4556
|
};
|
|
4556
4557
|
}
|
|
4557
4558
|
|
|
@@ -4563,12 +4564,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4563
4564
|
): TextureAsset {
|
|
4564
4565
|
return {
|
|
4565
4566
|
kind: 'texture',
|
|
4566
|
-
width,
|
|
4567
|
-
height,
|
|
4567
|
+
shape: { viewDimension: '2d', extent: { width, height } },
|
|
4568
4568
|
format,
|
|
4569
4569
|
data: new Uint8Array(width * height * 8),
|
|
4570
4570
|
colorSpace,
|
|
4571
|
-
|
|
4571
|
+
mips: { kind: 'none' },
|
|
4572
4572
|
};
|
|
4573
4573
|
}
|
|
4574
4574
|
|
|
@@ -4655,33 +4655,35 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4655
4655
|
const source = texturePod(false, 'rgba16float');
|
|
4656
4656
|
const res = deriveRenderDataTexture({
|
|
4657
4657
|
...source,
|
|
4658
|
-
data: new Uint8Array(source.width * source.height * 8),
|
|
4658
|
+
data: new Uint8Array(source.shape.extent.width * source.shape.extent.height * 8),
|
|
4659
4659
|
});
|
|
4660
4660
|
expect(res.ok).toBe(true);
|
|
4661
4661
|
if (!res.ok) return;
|
|
4662
|
-
expect(res.value.bytesPerRow).toBe(source.width * 8);
|
|
4662
|
+
expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 8);
|
|
4663
4663
|
});
|
|
4664
4664
|
|
|
4665
4665
|
it('derives row pitch from format when an rgba8 decoder retains trailing bytes', () => {
|
|
4666
4666
|
const source = texturePod(false, 'rgba8unorm');
|
|
4667
4667
|
const res = deriveRenderDataTexture({
|
|
4668
4668
|
...source,
|
|
4669
|
-
data: new Uint8Array(
|
|
4669
|
+
data: new Uint8Array(
|
|
4670
|
+
source.shape.extent.width * source.shape.extent.height * 4 + source.shape.extent.width,
|
|
4671
|
+
),
|
|
4670
4672
|
});
|
|
4671
4673
|
expect(res.ok).toBe(true);
|
|
4672
4674
|
if (!res.ok) return;
|
|
4673
|
-
expect(res.value.bytesPerRow).toBe(source.width * 4);
|
|
4675
|
+
expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 4);
|
|
4674
4676
|
});
|
|
4675
4677
|
|
|
4676
4678
|
it('derives the row pitch from an rgba32float source payload', () => {
|
|
4677
4679
|
const source = texturePod(false, 'rgba32float');
|
|
4678
4680
|
const res = deriveRenderDataTexture({
|
|
4679
4681
|
...source,
|
|
4680
|
-
data: new Uint8Array(source.width * source.height * 16),
|
|
4682
|
+
data: new Uint8Array(source.shape.extent.width * source.shape.extent.height * 16),
|
|
4681
4683
|
});
|
|
4682
4684
|
expect(res.ok).toBe(true);
|
|
4683
4685
|
if (!res.ok) return;
|
|
4684
|
-
expect(res.value.bytesPerRow).toBe(source.width * 16);
|
|
4686
|
+
expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 16);
|
|
4685
4687
|
});
|
|
4686
4688
|
|
|
4687
4689
|
it('rejects an uncompressed payload shorter than its base mip', () => {
|
|
@@ -5088,14 +5090,14 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5088
5090
|
expect(LIGHT_ARRAY_MAX_SLOTS).toBe(4);
|
|
5089
5091
|
});
|
|
5090
5092
|
|
|
5091
|
-
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 () => {
|
|
5092
5094
|
const { POINT_LIGHT_STD430_BYTES, SPOT_LIGHT_STD430_BYTES } = await import(
|
|
5093
5095
|
'../../../render/src/light-buffer-layout'
|
|
5094
5096
|
);
|
|
5095
5097
|
expect(POINT_LIGHT_STD430_BYTES).toBe(32);
|
|
5096
|
-
//
|
|
5097
|
-
//
|
|
5098
|
-
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);
|
|
5099
5101
|
});
|
|
5100
5102
|
});
|
|
5101
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)', () => {
|