@forgeax/engine-runtime 0.1.6 → 0.1.19
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 +61 -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__/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/backend-selection.d.ts.map +1 -1
- package/dist/collect-scene-asset.d.ts +6 -3
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +25 -4
- 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-guid-reverse.test.ts +33 -0
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +12 -11
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +39 -6
- package/src/__tests__/errors.unit.test.ts +60 -0
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +4 -5
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +461 -18
- package/src/__tests__/geometry.unit.test.ts +3 -1
- package/src/__tests__/helpers/standard-material-manifest.ts +45 -0
- package/src/__tests__/lights.unit.test.ts +15 -11
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +8 -14
- package/src/__tests__/materials.unit.test.ts +2 -0
- package/src/__tests__/pbr-pipeline.unit.test.ts +15 -12
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +6 -4
- package/src/__tests__/pipeline.unit.test.ts +5 -2
- package/src/__tests__/render-environment-consumer.integration.test.ts +33 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +93 -0
- package/src/__tests__/render-feature-prepared-graphics.browser.test.ts +5 -1
- package/src/__tests__/render-feature-prepared-graphics.fixture.ts +10 -0
- package/src/__tests__/render-system-mega.test.ts +3 -1
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +3 -1
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +3 -1
- 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__/renderer.test-d.ts +2 -0
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +14 -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__/skinned-shadow-mixed.dawn.test.ts +383 -0
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +10 -10
- package/src/__tests__/ssao-passes.test.ts +32 -12
- package/src/__tests__/systems.unit.test.ts +48 -31
- package/src/backend-selection.ts +4 -0
- package/src/collect-scene-asset.ts +14 -7
- package/src/index.ts +5 -1
- package/src/renderer-host.ts +30 -3
|
@@ -2232,10 +2232,11 @@ function spawnValidatedLight(
|
|
|
2232
2232
|
//
|
|
2233
2233
|
// visibility flags mirror the WebGPU GPUShaderStage bitmask:
|
|
2234
2234
|
// VERTEX = 0x1, FRAGMENT = 0x2.
|
|
2235
|
+
const VISIBILITY_VERTEX = 0x1;
|
|
2235
2236
|
const VISIBILITY_FRAGMENT = 0x2;
|
|
2236
2237
|
const VISIBILITY_VERTEX_FRAGMENT = 0x1 | 0x2;
|
|
2237
2238
|
|
|
2238
|
-
describe('binding 8 = spotShadowMap (D-5 always-on
|
|
2239
|
+
describe('binding 8 = spotShadowMap (D-5 always-on shadow entry)', () => {
|
|
2239
2240
|
it('declares binding 8 as a depth 2D texture, FRAGMENT-only', () => {
|
|
2240
2241
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2241
2242
|
const b8 = entries.find((e) => e.binding === 8);
|
|
@@ -2253,10 +2254,12 @@ function spawnValidatedLight(
|
|
|
2253
2254
|
expect(noStorage.some((e) => e.binding === 8)).toBe(true);
|
|
2254
2255
|
});
|
|
2255
2256
|
|
|
2256
|
-
it('binding
|
|
2257
|
+
it('reserves binding 9 and keeps Points/Lines at binding 10', () => {
|
|
2257
2258
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2258
2259
|
expect(entries.some((e) => e.binding === 9)).toBe(false);
|
|
2259
|
-
|
|
2260
|
+
const pointsLines = entries.find((e) => e.binding === 10);
|
|
2261
|
+
expect(pointsLines?.buffer?.type).toBe('uniform');
|
|
2262
|
+
expect(pointsLines?.visibility).toBe(VISIBILITY_VERTEX);
|
|
2260
2263
|
});
|
|
2261
2264
|
});
|
|
2262
2265
|
|
|
@@ -2272,18 +2275,19 @@ function spawnValidatedLight(
|
|
|
2272
2275
|
// UBO (binding 0). This block locks that the spot matrices add ZERO new view
|
|
2273
2276
|
// BGL buffer bindings (the WebGL2 budget invariant).
|
|
2274
2277
|
describe('spotLightViewProj folded into View UBO (binding 0), not a new binding', () => {
|
|
2275
|
-
it('view BGL
|
|
2278
|
+
it('view BGL includes only the vertex Points/Lines UBO beyond binding 7', () => {
|
|
2276
2279
|
// Enumerate uniform-buffer entries on the view BGL. After the w25 fold,
|
|
2277
|
-
//
|
|
2278
|
-
//
|
|
2279
|
-
// (shadowCasterCascade)
|
|
2280
|
+
// uniform buffers are binding 0 (View UBO, carries the spot matrices
|
|
2281
|
+
// in its tail), binding 6 (point shadowParams), binding 7
|
|
2282
|
+
// (shadowCasterCascade), and binding 10 (Points/Lines view). No
|
|
2283
|
+
// standalone spot-matrix uniform buffer exists.
|
|
2280
2284
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2281
2285
|
const uniformBufferBindings = entries
|
|
2282
2286
|
.filter((e) => e.buffer?.type === 'uniform')
|
|
2283
2287
|
.map((e) => e.binding)
|
|
2284
2288
|
.sort((a, b) => a - b);
|
|
2285
2289
|
// storageBuffer=true: bindings 1+2 are read-only-storage (not uniform).
|
|
2286
|
-
expect(uniformBufferBindings).toEqual([0, 6, 7]);
|
|
2290
|
+
expect(uniformBufferBindings).toEqual([0, 6, 7, 10]);
|
|
2287
2291
|
});
|
|
2288
2292
|
|
|
2289
2293
|
it('storageBuffer=false: WebGL2 fallback uniform-buffer bindings stay within budget', () => {
|
|
@@ -2348,12 +2352,12 @@ function spawnValidatedLight(
|
|
|
2348
2352
|
});
|
|
2349
2353
|
});
|
|
2350
2354
|
|
|
2351
|
-
describe('full binding roster
|
|
2352
|
-
it('exposes
|
|
2355
|
+
describe('full binding roster reserves 9 for Points/Lines at 10', () => {
|
|
2356
|
+
it('exposes bindings 0..8 plus binding 10, with binding 9 reserved', () => {
|
|
2353
2357
|
const bindings = buildPbrViewBglEntries({ storageBuffer: true })
|
|
2354
2358
|
.map((e) => e.binding)
|
|
2355
2359
|
.sort((a, b) => a - b);
|
|
2356
|
-
expect(bindings).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
|
2360
|
+
expect(bindings).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 10]);
|
|
2357
2361
|
});
|
|
2358
2362
|
});
|
|
2359
2363
|
});
|
|
@@ -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,
|
|
@@ -22,11 +21,9 @@ describe('material texture UV scale [w37]', () => {
|
|
|
22
21
|
expect(materialTextureUvScale({ width: 17, height: 9, format: 'rgba8unorm' })).toEqual([1, 1]);
|
|
23
22
|
});
|
|
24
23
|
|
|
25
|
-
it('keeps
|
|
26
|
-
expect(userRegionTextureFieldOrder([])).toEqual(
|
|
27
|
-
expect(userRegionTextureFieldOrder([{ name: 'baseColor', type: 'color' }])).toEqual(
|
|
28
|
-
BUILTIN_USER_REGION_TEXTURE_FIELDS,
|
|
29
|
-
);
|
|
24
|
+
it('keeps compatibility param schemas compact when they have no textures', () => {
|
|
25
|
+
expect(userRegionTextureFieldOrder([])).toEqual([]);
|
|
26
|
+
expect(userRegionTextureFieldOrder([{ name: 'baseColor', type: 'color' }])).toEqual([]);
|
|
30
27
|
});
|
|
31
28
|
|
|
32
29
|
it('writes each slot set and transform into the builtin PBR payload', () => {
|
|
@@ -61,8 +58,8 @@ describe('material texture UV scale [w37]', () => {
|
|
|
61
58
|
new World(),
|
|
62
59
|
);
|
|
63
60
|
const f32 = new Float32Array(payload.buffer, payload.byteOffset, payload.byteLength / 4);
|
|
64
|
-
expect(Array.from(f32.slice(
|
|
65
|
-
expect(Array.from(f32.slice(
|
|
61
|
+
expect(Array.from(f32.slice(32, 40))).toEqual([0.125, 0.25, 2, 3, 1, 0.5, 1, 1]);
|
|
62
|
+
expect(Array.from(f32.slice(48, 56))).toEqual([0.75, 0.25, 4, 5, 0, 0, 1, 1]);
|
|
66
63
|
});
|
|
67
64
|
|
|
68
65
|
it('keeps identity UV records for metadata-free standard and builtin schema materials', () => {
|
|
@@ -85,14 +82,11 @@ describe('material texture UV scale [w37]', () => {
|
|
|
85
82
|
expect(
|
|
86
83
|
Array.from(
|
|
87
84
|
new Float32Array(standard.buffer, standard.byteOffset, standard.byteLength / 4).slice(
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
32,
|
|
86
|
+
96,
|
|
90
87
|
),
|
|
91
88
|
),
|
|
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
|
-
]);
|
|
89
|
+
).toEqual(Array.from({ length: 8 }, () => [0, 0, 1, 1, 0, 0, 1, 1]).flat());
|
|
96
90
|
|
|
97
91
|
const unlit = buildPbrMaterialUboPayload({
|
|
98
92
|
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
|
|
|
@@ -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: 10 entries with read-only-storage on bindings 1+2', () => {
|
|
48
48
|
const spec = makeSpec();
|
|
49
49
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
50
50
|
kind: 'pbr-view',
|
|
@@ -56,12 +56,15 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
56
56
|
};
|
|
57
57
|
expect(out).toEqual(expected);
|
|
58
58
|
// feat-20260625-spot-light-shadow-mapping: binding 8 (spot shadow atlas,
|
|
59
|
-
// w14) raised the view BGL to 9 entries (always-on, D-5).
|
|
59
|
+
// w14) raised the view BGL to 9 entries (always-on, D-5). The
|
|
60
|
+
// Points/Lines viewport UBO occupies binding 10, while w25 (scope-amend
|
|
60
61
|
// webkit-fallback) folded the former binding 9 spotLightViewProj matrices
|
|
61
62
|
// UBO into the View UBO (`view.spotLightViewProj`), so the BGL stays at 9
|
|
62
|
-
// entries (bindings 0..8) — keeping the WebGL2 fallback fragment uniform-
|
|
63
|
+
// entries (bindings 0..8 plus binding 10) — keeping the WebGL2 fallback fragment uniform-
|
|
63
64
|
// buffer count <= 11 (GLES 3.0).
|
|
64
|
-
expect(out.entries.length).toBe(
|
|
65
|
+
expect(out.entries.length).toBe(10);
|
|
66
|
+
expect(out.entries.find((entry) => entry.binding === 9)).toBeUndefined();
|
|
67
|
+
expect(out.entries.find((entry) => entry.binding === 10)?.buffer?.type).toBe('uniform');
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
it('storageBuffer=false: bindings 1+2 fall back to uniform', () => {
|
|
@@ -79,22 +82,22 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
79
82
|
});
|
|
80
83
|
|
|
81
84
|
describe('pbr-material-merged', () => {
|
|
82
|
-
it('
|
|
85
|
+
it('26 entries: user-region 17 (derived) + ibl 7 + transmission 2', () => {
|
|
83
86
|
const spec = makeSpec();
|
|
84
87
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
85
88
|
kind: 'pbr-material-merged',
|
|
86
89
|
});
|
|
87
90
|
// Post-M2 (D-1): user-region comes from derive(paramSchema).bglEntries
|
|
88
|
-
// (built-in standard-PBR
|
|
89
|
-
// appended at start = userRegion.length.
|
|
91
|
+
// (built-in standard-PBR 8-texture contract), then IBL + transmission
|
|
92
|
+
// are appended at start = userRegion.length.
|
|
90
93
|
const userRegion = buildPbrMaterialUserRegionEntries();
|
|
91
94
|
const afterIbl = [...userRegion, ...appendInjection(userRegion, 'ibl')];
|
|
92
95
|
const expected = {
|
|
93
96
|
label: 'pbr-material-skylight-bgl',
|
|
94
|
-
entries: [...afterIbl, ...appendInjection(afterIbl, '
|
|
97
|
+
entries: [...afterIbl, ...appendInjection(afterIbl, 'transmission')],
|
|
95
98
|
};
|
|
96
99
|
expect(out).toEqual(expected);
|
|
97
|
-
expect(out.entries.length).toBe(
|
|
100
|
+
expect(out.entries.length).toBe(26);
|
|
98
101
|
});
|
|
99
102
|
});
|
|
100
103
|
|
|
@@ -139,7 +142,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
139
142
|
entries: [
|
|
140
143
|
{
|
|
141
144
|
binding: 0,
|
|
142
|
-
visibility:
|
|
145
|
+
visibility: 0x3,
|
|
143
146
|
buffer: { type: 'read-only-storage', hasDynamicOffset: false },
|
|
144
147
|
},
|
|
145
148
|
],
|
|
@@ -182,7 +185,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
182
185
|
});
|
|
183
186
|
|
|
184
187
|
describe('unlit-material', () => {
|
|
185
|
-
it('
|
|
188
|
+
it('17 entries: base PBR material only (no skylight injection)', () => {
|
|
186
189
|
const spec = makeSpec();
|
|
187
190
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
188
191
|
kind: 'unlit-material',
|
|
@@ -191,7 +194,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
191
194
|
label: 'unlit-material-bgl',
|
|
192
195
|
entries: buildPbrMaterialUserRegionEntries(),
|
|
193
196
|
});
|
|
194
|
-
expect(out.entries.length).toBe(
|
|
197
|
+
expect(out.entries.length).toBe(17);
|
|
195
198
|
});
|
|
196
199
|
});
|
|
197
200
|
});
|
|
@@ -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',
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const environmentSource = readFileSync(
|
|
6
|
+
fileURLToPath(new URL('../../../render/src/extract/environment.ts', import.meta.url)),
|
|
7
|
+
'utf8',
|
|
8
|
+
);
|
|
9
|
+
const vfxSource = readFileSync(
|
|
10
|
+
fileURLToPath(
|
|
11
|
+
new URL('../../../vfx-render/src/feature/gpu-particle-feature.ts', import.meta.url),
|
|
12
|
+
),
|
|
13
|
+
'utf8',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe('runtime Environment consumer ownership', () => {
|
|
17
|
+
it('consumes extracted Environment facts without a second Fog resource topology', () => {
|
|
18
|
+
expect(environmentSource).toContain('createEnvironmentExtractionContext');
|
|
19
|
+
expect(environmentSource).not.toContain('createFullscreenRenderFeature');
|
|
20
|
+
expect(vfxSource).not.toMatch(/fog.*(history|counter|upload)/i);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('indexes the public recovery sequence without exposing graph or device state', () => {
|
|
24
|
+
const runtimeReadme = readFileSync(
|
|
25
|
+
fileURLToPath(new URL('../../README.md', import.meta.url)),
|
|
26
|
+
'utf8',
|
|
27
|
+
);
|
|
28
|
+
expect(runtimeReadme).toContain('inspect()');
|
|
29
|
+
expect(runtimeReadme).toContain('typed `detail`');
|
|
30
|
+
expect(runtimeReadme).toMatch(/retry the same\s+request/);
|
|
31
|
+
expect(runtimeReadme).toContain('graph, device, or');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -38,14 +38,30 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
38
38
|
return code;
|
|
39
39
|
case 'cleanup-failed':
|
|
40
40
|
return code;
|
|
41
|
+
case 'taa-unavailable':
|
|
42
|
+
return code;
|
|
41
43
|
case 'frame-receipt-stale':
|
|
42
44
|
return code;
|
|
43
45
|
case 'renderer-contract-failed':
|
|
44
46
|
return code;
|
|
45
47
|
case 'observation-unavailable':
|
|
46
48
|
return code;
|
|
49
|
+
case 'scene-data-unavailable':
|
|
50
|
+
return code;
|
|
47
51
|
case 'shadow-invalid-config':
|
|
48
52
|
return code;
|
|
53
|
+
case 'environment-source-conflict':
|
|
54
|
+
return code;
|
|
55
|
+
case 'fog-cardinality':
|
|
56
|
+
return code;
|
|
57
|
+
case 'taa-caps-insufficient':
|
|
58
|
+
return code;
|
|
59
|
+
case 'environment-generation-failed':
|
|
60
|
+
return code;
|
|
61
|
+
case 'atmosphere-invalid-parameter':
|
|
62
|
+
return code;
|
|
63
|
+
case 'owner-stage-failed':
|
|
64
|
+
return code;
|
|
49
65
|
case 'equirect-projection-failed':
|
|
50
66
|
return code;
|
|
51
67
|
case 'hdrp-light-budget-exceeded':
|
|
@@ -84,6 +100,20 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
|
|
|
84
100
|
return code;
|
|
85
101
|
case 'render-feature-draw-recording-failed':
|
|
86
102
|
return code;
|
|
103
|
+
case 'transmission-capability-missing':
|
|
104
|
+
return code;
|
|
105
|
+
case 'points-lines-invalid-style':
|
|
106
|
+
return code;
|
|
107
|
+
case 'points-lines-topology-mismatch':
|
|
108
|
+
return code;
|
|
109
|
+
case 'points-lines-style-unsupported':
|
|
110
|
+
return code;
|
|
111
|
+
case 'points-lines-material-unsupported':
|
|
112
|
+
return code;
|
|
113
|
+
case 'points-lines-budget-exceeded':
|
|
114
|
+
return code;
|
|
115
|
+
case 'points-lines-prepare-failed':
|
|
116
|
+
return code;
|
|
87
117
|
default: {
|
|
88
118
|
const exhaustive: never = code;
|
|
89
119
|
return exhaustive;
|
|
@@ -159,10 +189,41 @@ function narrowRenderError(err: RenderError): void {
|
|
|
159
189
|
void err.detail.reason;
|
|
160
190
|
void err.detail.recovery;
|
|
161
191
|
break;
|
|
192
|
+
case 'scene-data-unavailable':
|
|
193
|
+
void err.detail.featureIdentity;
|
|
194
|
+
void err.detail.schema;
|
|
195
|
+
void err.detail.lane;
|
|
196
|
+
void err.detail.reason;
|
|
197
|
+
void err.detail.missingContributorIds;
|
|
198
|
+
void err.detail.omittedMissingContributorCount;
|
|
199
|
+
void err.detail.recovery;
|
|
200
|
+
break;
|
|
162
201
|
case 'shadow-invalid-config':
|
|
163
202
|
void err.detail.field; // string
|
|
164
203
|
void err.detail.value; // number
|
|
165
204
|
break;
|
|
205
|
+
case 'environment-source-conflict':
|
|
206
|
+
void err.detail.owners;
|
|
207
|
+
break;
|
|
208
|
+
case 'fog-cardinality':
|
|
209
|
+
void err.detail.count;
|
|
210
|
+
break;
|
|
211
|
+
case 'taa-caps-insufficient':
|
|
212
|
+
void err.detail.required;
|
|
213
|
+
void err.detail.available;
|
|
214
|
+
break;
|
|
215
|
+
case 'environment-generation-failed':
|
|
216
|
+
void err.detail.sourceKey;
|
|
217
|
+
void err.detail.stage;
|
|
218
|
+
break;
|
|
219
|
+
case 'atmosphere-invalid-parameter':
|
|
220
|
+
void err.detail.field;
|
|
221
|
+
void err.detail.value;
|
|
222
|
+
break;
|
|
223
|
+
case 'owner-stage-failed':
|
|
224
|
+
void err.detail.owner;
|
|
225
|
+
void err.detail.stage;
|
|
226
|
+
break;
|
|
166
227
|
case 'equirect-projection-failed':
|
|
167
228
|
void err.detail.handle; // number
|
|
168
229
|
break;
|
|
@@ -235,11 +296,43 @@ function narrowRenderError(err: RenderError): void {
|
|
|
235
296
|
void err.detail.backendReason;
|
|
236
297
|
void err.detail.operation;
|
|
237
298
|
break;
|
|
299
|
+
case 'taa-unavailable':
|
|
300
|
+
void err.detail.reason;
|
|
301
|
+
break;
|
|
238
302
|
case 'vertex-color-variant-conflict':
|
|
239
303
|
void err.detail.authored;
|
|
240
304
|
void err.detail.authoredValue;
|
|
241
305
|
void err.detail.projected;
|
|
242
306
|
break;
|
|
307
|
+
case 'points-lines-invalid-style':
|
|
308
|
+
void err.detail.component;
|
|
309
|
+
void err.detail.field;
|
|
310
|
+
break;
|
|
311
|
+
case 'points-lines-topology-mismatch':
|
|
312
|
+
void err.detail.submesh;
|
|
313
|
+
void err.detail.actual;
|
|
314
|
+
break;
|
|
315
|
+
case 'points-lines-style-unsupported':
|
|
316
|
+
void err.detail.member;
|
|
317
|
+
void err.detail.supported;
|
|
318
|
+
break;
|
|
319
|
+
case 'points-lines-material-unsupported':
|
|
320
|
+
void err.detail.material;
|
|
321
|
+
void err.detail.pass;
|
|
322
|
+
break;
|
|
323
|
+
case 'points-lines-budget-exceeded':
|
|
324
|
+
void err.detail.requested;
|
|
325
|
+
void err.detail.limit;
|
|
326
|
+
break;
|
|
327
|
+
case 'points-lines-prepare-failed':
|
|
328
|
+
void err.detail.owner;
|
|
329
|
+
void err.detail.generation;
|
|
330
|
+
break;
|
|
331
|
+
case 'transmission-capability-missing':
|
|
332
|
+
void err.detail.material;
|
|
333
|
+
void err.detail.capability;
|
|
334
|
+
void err.detail.stage;
|
|
335
|
+
break;
|
|
243
336
|
default: {
|
|
244
337
|
const exhaustive: never = err;
|
|
245
338
|
void exhaustive;
|
|
@@ -45,6 +45,10 @@ describe('prepared graphics browser contract', () => {
|
|
|
45
45
|
canvas.remove();
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// This negative path intentionally exercises a real WebGPU pipeline before
|
|
49
|
+
// the renderer reports the structured stage error. Its cold lavapipe start
|
|
50
|
+
// has exceeded Vitest's 15s default in CI; keep the assertion bounded at the
|
|
51
|
+
// owner rather than weakening the global browser timeout.
|
|
48
52
|
it('reports forged prepared state through subscribe without renderer diagnostics getters', async () => {
|
|
49
53
|
if (typeof navigator?.gpu?.requestAdapter !== 'function') {
|
|
50
54
|
throw new Error('WebGPU is unavailable');
|
|
@@ -80,5 +84,5 @@ describe('prepared graphics browser contract', () => {
|
|
|
80
84
|
});
|
|
81
85
|
expect(renderer).not.toHaveProperty('renderFeatureDiagnostics');
|
|
82
86
|
canvas.remove();
|
|
83
|
-
});
|
|
87
|
+
}, 30_000);
|
|
84
88
|
});
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { Camera } from '@forgeax/engine-render';
|
|
9
9
|
import { Transform } from '@forgeax/engine-scene';
|
|
10
10
|
import { ok } from '@forgeax/engine-types';
|
|
11
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
11
12
|
|
|
12
13
|
export type PreparedFeatureMode = 'accepted' | 'empty' | 'mismatch' | 'recovery';
|
|
13
14
|
|
|
@@ -19,6 +20,15 @@ export const preparedManifest = `data:application/json,${encodeURIComponent(
|
|
|
19
20
|
{ hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
|
|
20
21
|
{ hash: 'tonemap0', wgsl: '/* tonemap stub */', glsl: '', bindings: '' },
|
|
21
22
|
],
|
|
23
|
+
materialShaders: [
|
|
24
|
+
{
|
|
25
|
+
identifier: 'forgeax::default-standard-pbr',
|
|
26
|
+
sourcePath: 'forgeax::default-standard-pbr.wgsl',
|
|
27
|
+
composedWgsl: '/* stub */',
|
|
28
|
+
paramSchema: '[]',
|
|
29
|
+
variants: standardMaterialShaderVariants(),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
22
32
|
}),
|
|
23
33
|
)}`;
|
|
24
34
|
|
|
@@ -20,6 +20,7 @@ import { Transform } from '@forgeax/engine-scene';
|
|
|
20
20
|
import type { Handle } from '@forgeax/engine-types';
|
|
21
21
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
22
22
|
import { extractFrame, prepareExtractContext } from '../../../render/src/render-system-extract';
|
|
23
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
23
24
|
import { drawWithOwners } from './renderer-test-utils';
|
|
24
25
|
|
|
25
26
|
type RendererErrorObservation = {
|
|
@@ -278,7 +279,8 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
278
279
|
sourcePath: `${identifier}.wgsl`,
|
|
279
280
|
composedWgsl: '/* stub */',
|
|
280
281
|
paramSchema: '[]',
|
|
281
|
-
variants:
|
|
282
|
+
variants:
|
|
283
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
282
284
|
});
|
|
283
285
|
const manifest = {
|
|
284
286
|
schemaVersion: '1.0.0',
|
|
@@ -33,6 +33,7 @@ import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
|
33
33
|
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
34
34
|
import type { Handle, MaterialAsset, MeshAsset, TextureAsset } from '@forgeax/engine-types';
|
|
35
35
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
36
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
36
37
|
|
|
37
38
|
function canonicalTestMeshAttributes(vertexCount: number) {
|
|
38
39
|
return {
|
|
@@ -195,7 +196,8 @@ function buildManifestDataUrl(): string {
|
|
|
195
196
|
sourcePath: `${identifier}.wgsl`,
|
|
196
197
|
composedWgsl: '/* stub */',
|
|
197
198
|
paramSchema,
|
|
198
|
-
variants:
|
|
199
|
+
variants:
|
|
200
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
199
201
|
});
|
|
200
202
|
// The standard-PBR shader declares baseColorTexture / metallicRoughnessTexture
|
|
201
203
|
// / normalTexture as texture2d params. Texture resolution is now schema-driven
|
|
@@ -26,6 +26,7 @@ import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
|
26
26
|
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
27
27
|
import type { Handle, MaterialAsset, MeshAsset, TextureAsset } from '@forgeax/engine-types';
|
|
28
28
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
29
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
29
30
|
|
|
30
31
|
function canonicalTestMeshAttributes(vertexCount: number) {
|
|
31
32
|
return {
|
|
@@ -161,7 +162,8 @@ function buildManifestDataUrl(): string {
|
|
|
161
162
|
sourcePath: `${identifier}.wgsl`,
|
|
162
163
|
composedWgsl: '/* stub */',
|
|
163
164
|
paramSchema,
|
|
164
|
-
variants:
|
|
165
|
+
variants:
|
|
166
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
165
167
|
});
|
|
166
168
|
const pbrParamSchema = JSON.stringify([
|
|
167
169
|
{ name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
|
|
@@ -55,7 +55,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
55
55
|
// asserts the PBR baseline against literal byte values.
|
|
56
56
|
//
|
|
57
57
|
// Coverage:
|
|
58
|
-
// -
|
|
58
|
+
// - 384 B payload (STANDARD_PBR_UBO_SIZE)
|
|
59
59
|
// - slot 0 = baseColor.rgb + 1 (alpha hardcoded)
|
|
60
60
|
// - slot 1 first 8 B = metallic, roughness (f32x2)
|
|
61
61
|
// - slot 1 last 4 u32 (channelMap) = [2, 1, 0, 0]
|
|
@@ -108,7 +108,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
108
108
|
expect(typeof mod.buildPbrMaterialUboPayload).toBe('function');
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
it('
|
|
111
|
+
it('384 B payload size + standard PBR slot layout (baseline)', () => {
|
|
112
112
|
if (typeof mod.buildPbrMaterialUboPayload !== 'function') {
|
|
113
113
|
throw new Error('helper not exported yet');
|
|
114
114
|
}
|
|
@@ -124,7 +124,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
124
124
|
specularTint: [0.11, 0.22, 0.33],
|
|
125
125
|
});
|
|
126
126
|
const buf = mod.buildPbrMaterialUboPayload(snap);
|
|
127
|
-
expect(buf.byteLength).toBe(
|
|
127
|
+
expect(buf.byteLength).toBe(384);
|
|
128
128
|
const f32 = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
|
|
129
129
|
// feat-20260613 fix-issue-1 (D-8 channelMap split): the 4 channelMap
|
|
130
130
|
// u32 slots collapse into 4 independent f32 channel selectors at
|
|
@@ -161,7 +161,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
161
161
|
expect(f32[20]).toBeCloseTo(0.11);
|
|
162
162
|
expect(f32[21]).toBeCloseTo(0.22);
|
|
163
163
|
expect(f32[22]).toBeCloseTo(0.33);
|
|
164
|
-
|
|
164
|
+
// normalScale remains at the scalar end of the authored region after
|
|
165
|
+
// transmission fields moved the engine-owned coordinate records.
|
|
166
|
+
expect(f32[23]).toBe(1);
|
|
165
167
|
});
|
|
166
168
|
|
|
167
169
|
it('writes schema-carried alphaCutoff into the baseline PBR payload', () => {
|
|
@@ -180,15 +182,15 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
180
182
|
const apply = mod.applyMaterialTextureUvScales;
|
|
181
183
|
if (typeof apply !== 'function') throw new Error('helper not exported yet');
|
|
182
184
|
const world = new World();
|
|
183
|
-
const pbrPayload = new ArrayBuffer(
|
|
185
|
+
const pbrPayload = new ArrayBuffer(384);
|
|
184
186
|
apply(pbrPayload, makePbrSnapshot({}), world);
|
|
185
187
|
const pbrF32 = new Float32Array(pbrPayload);
|
|
186
|
-
expect(pbrF32[26]).toBe(1);
|
|
187
188
|
expect(pbrF32[34]).toBe(1);
|
|
188
189
|
expect(pbrF32[42]).toBe(1);
|
|
189
190
|
expect(pbrF32[50]).toBe(1);
|
|
190
191
|
expect(pbrF32[58]).toBe(1);
|
|
191
192
|
expect(pbrF32[66]).toBe(1);
|
|
193
|
+
expect(pbrF32[74]).toBe(1);
|
|
192
194
|
|
|
193
195
|
const spritePayload = new ArrayBuffer(128);
|
|
194
196
|
apply(
|