@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
|
@@ -47,7 +47,7 @@ function makeSpec(shaderId: string): PipelineSpec {
|
|
|
47
47
|
|
|
48
48
|
describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
49
49
|
describe('pbr-view BGL (light buffers; binding 1+2)', () => {
|
|
50
|
-
it('sprite-lit vs sprite share the same
|
|
50
|
+
it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under storage-buffer caps', () => {
|
|
51
51
|
const spriteSpec = makeSpec('forgeax::sprite');
|
|
52
52
|
const spriteLitSpec = makeSpec('forgeax::sprite-lit');
|
|
53
53
|
const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
|
|
@@ -59,10 +59,10 @@ describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
|
59
59
|
caps: { storageBuffer: true },
|
|
60
60
|
});
|
|
61
61
|
expect(JSON.stringify(spriteLit)).toBe(JSON.stringify(sprite));
|
|
62
|
-
expect(spriteLit.entries.length).toBe(
|
|
62
|
+
expect(spriteLit.entries.length).toBe(10); // binding 10 is the shared Points/Lines view UBO
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it('sprite-lit vs sprite share the same
|
|
65
|
+
it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under uniform fallback caps (AC-10)', () => {
|
|
66
66
|
const spriteSpec = makeSpec('forgeax::sprite');
|
|
67
67
|
const spriteLitSpec = makeSpec('forgeax::sprite-lit');
|
|
68
68
|
const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
|
|
@@ -118,24 +118,24 @@ describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
describe('material BGL congruence (pbr-material-merged / unlit-material)', () => {
|
|
121
|
-
it('pbr-material-user-region entries are
|
|
121
|
+
it('pbr-material-user-region entries are 17 (PBR layout reused by sprite & sprite-lit)', () => {
|
|
122
122
|
// sprite + sprite-lit use the default standard-PBR user-region
|
|
123
|
-
// schema (
|
|
123
|
+
// schema (20 value fields + 8 texture fields produce 17 BGL entries).
|
|
124
124
|
// Both share the same userRegion shape and therefore the same BGL.
|
|
125
125
|
const base = buildPbrMaterialUserRegionEntries();
|
|
126
|
-
expect(base.length).toBe(
|
|
126
|
+
expect(base.length).toBe(17);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
it('pbr-material-merged stays
|
|
129
|
+
it('pbr-material-merged stays 26 entries (sprite/sprite-lit do not change material BGL shape)', () => {
|
|
130
130
|
const merged = buildBindGroupLayoutDescriptor(makeSpec('forgeax::sprite-lit'), {
|
|
131
131
|
kind: 'pbr-material-merged',
|
|
132
132
|
});
|
|
133
|
-
expect(merged.entries.length).toBe(
|
|
134
|
-
// mergeSkylightIntoMaterialBgl +
|
|
133
|
+
expect(merged.entries.length).toBe(26);
|
|
134
|
+
// mergeSkylightIntoMaterialBgl + transmission injection must stay
|
|
135
135
|
// append-only against the 7-entry PBR base.
|
|
136
136
|
const base = buildPbrMaterialUserRegionEntries();
|
|
137
137
|
const afterSky = mergeSkylightIntoMaterialBgl(base);
|
|
138
|
-
const expected = [...afterSky, ...appendInjection(afterSky, '
|
|
138
|
+
const expected = [...afterSky, ...appendInjection(afterSky, 'transmission')];
|
|
139
139
|
expect(merged.entries).toEqual(expected);
|
|
140
140
|
});
|
|
141
141
|
});
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { RenderGraph } from '@forgeax/engine-render-graph';
|
|
9
9
|
import type { RhiDevice } from '@forgeax/engine-rhi';
|
|
10
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { DeviceScope } from '../../../render/src/device/device-scope';
|
|
12
|
+
import type { BloomPersistentBundle } from '../../../render/src/record/render-context';
|
|
11
13
|
import {
|
|
12
14
|
recordBloomBlurHPass,
|
|
13
15
|
recordBloomBlurVPass,
|
|
@@ -680,6 +682,31 @@ function makePostProcessRecordCtx(): {
|
|
|
680
682
|
queue: { writeBuffer: vi.fn(() => ({ ok: true, value: undefined })) },
|
|
681
683
|
};
|
|
682
684
|
const sampler = { label: 'postprocess-sampler' };
|
|
685
|
+
const bloomScope = DeviceScope.create(1, 'runtime-ssao-bloom-fixture');
|
|
686
|
+
const ownBloomHandle = (
|
|
687
|
+
kind: 'pipeline' | 'binding' | 'post-effect' | 'buffer',
|
|
688
|
+
label: string,
|
|
689
|
+
) => {
|
|
690
|
+
const handle = { label };
|
|
691
|
+
bloomScope._adopt(kind, handle, () => undefined);
|
|
692
|
+
return handle;
|
|
693
|
+
};
|
|
694
|
+
const bloomResources = {
|
|
695
|
+
scope: bloomScope,
|
|
696
|
+
generation: bloomScope.generation,
|
|
697
|
+
bloomBrightPipeline: ownBloomHandle('pipeline', 'bloom-bright-pipeline'),
|
|
698
|
+
bloomBlurHPipeline: ownBloomHandle('pipeline', 'bloom-blur-h-pipeline'),
|
|
699
|
+
bloomBlurVPipeline: ownBloomHandle('pipeline', 'bloom-blur-v-pipeline'),
|
|
700
|
+
bloomCompositePipeline: ownBloomHandle('pipeline', 'bloom-composite-pipeline'),
|
|
701
|
+
bloomBrightBindGroupLayout: ownBloomHandle('binding', 'bloom-bright-bgl'),
|
|
702
|
+
bloomBlurBindGroupLayout: ownBloomHandle('binding', 'bloom-blur-bgl'),
|
|
703
|
+
bloomCompositeBindGroupLayout: ownBloomHandle('binding', 'bloom-composite-bgl'),
|
|
704
|
+
bloomSampler: ownBloomHandle('post-effect', 'bloom-sampler'),
|
|
705
|
+
bloomBrightParamsBuffer: ownBloomHandle('buffer', 'bloom-bright-params'),
|
|
706
|
+
bloomBlurHParamsBuffer: ownBloomHandle('buffer', 'bloom-blur-h-params'),
|
|
707
|
+
bloomBlurVParamsBuffer: ownBloomHandle('buffer', 'bloom-blur-v-params'),
|
|
708
|
+
bloomCompositeParamsBuffer: ownBloomHandle('buffer', 'bloom-composite-params'),
|
|
709
|
+
} as unknown as BloomPersistentBundle;
|
|
683
710
|
const ctx = {
|
|
684
711
|
runtime: {
|
|
685
712
|
device: { ...device, caps: { storageBuffer: true } },
|
|
@@ -702,23 +729,12 @@ function makePostProcessRecordCtx(): {
|
|
|
702
729
|
skyboxBindGroupLayout: { label: 'skybox-bgl' },
|
|
703
730
|
skyboxSampler: sampler,
|
|
704
731
|
skyboxRotationBuffer: { label: 'skybox-rotation' },
|
|
705
|
-
bloomBrightPipeline: { label: 'bloom-bright-pipeline' },
|
|
706
|
-
bloomBrightBindGroupLayout: { label: 'bloom-bright-bgl' },
|
|
707
|
-
bloomBrightParamsBuffer: { label: 'bloom-bright-params' },
|
|
708
|
-
bloomBlurHPipeline: { label: 'bloom-blur-h-pipeline' },
|
|
709
|
-
bloomBlurVPipeline: { label: 'bloom-blur-v-pipeline' },
|
|
710
|
-
bloomBlurBindGroupLayout: { label: 'bloom-blur-bgl' },
|
|
711
|
-
bloomBlurHParamsBuffer: { label: 'bloom-blur-h-params' },
|
|
712
|
-
bloomBlurVParamsBuffer: { label: 'bloom-blur-v-params' },
|
|
713
|
-
bloomCompositePipeline: { label: 'bloom-composite-pipeline' },
|
|
714
|
-
bloomCompositeBindGroupLayout: { label: 'bloom-composite-bgl' },
|
|
715
|
-
bloomCompositeParamsBuffer: { label: 'bloom-composite-params' },
|
|
716
|
-
bloomSampler: sampler,
|
|
717
732
|
fxaaPipeline: { label: 'fxaa-pipeline' },
|
|
718
733
|
fxaaBindGroupLayout: { label: 'fxaa-bgl' },
|
|
719
734
|
fxaaSampler: sampler,
|
|
720
735
|
},
|
|
721
736
|
},
|
|
737
|
+
bloomResources,
|
|
722
738
|
frameState: { postProcessBgCache: new WeakMap() },
|
|
723
739
|
bindGroupCounts: { createBindGroup: 0, keys: [] },
|
|
724
740
|
camera: {
|
|
@@ -778,6 +794,10 @@ describe('post-process bindgroup identity cache (issue #670)', () => {
|
|
|
778
794
|
views.bloomBlurV,
|
|
779
795
|
);
|
|
780
796
|
expect(bindGroupCreates[5]?.entries[0]?.resource.value).toBe(views.cubemap);
|
|
797
|
+
expect(bindGroupCreates[5]?.entries[2]?.resource.value).toEqual({
|
|
798
|
+
buffer: ctx.pipelineState.viewUniformBuffer,
|
|
799
|
+
size: 960,
|
|
800
|
+
});
|
|
781
801
|
|
|
782
802
|
recordAll();
|
|
783
803
|
expect(bindGroupCreates).toHaveLength(6);
|
|
@@ -120,6 +120,7 @@ import { INSTANCE_STORAGE_STRIDE_FLOATS } from '../../../render/src/record/mesh-
|
|
|
120
120
|
import { selectSwapChainFormat } from '../../../render/src/render-system';
|
|
121
121
|
import { createSkinPaletteAllocator } from '../../../render/src/systems/skin-palette-allocator';
|
|
122
122
|
import type { TransparentEntry } from '../../../render/src/systems/transparent-sort-config';
|
|
123
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
123
124
|
import { drawWithOwners } from './renderer-test-utils';
|
|
124
125
|
|
|
125
126
|
function componentFieldType(component: { fields: Record<string, { type: string }> }, name: string) {
|
|
@@ -1645,7 +1646,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1645
1646
|
sourcePath: `${identifier}.wgsl`,
|
|
1646
1647
|
composedWgsl,
|
|
1647
1648
|
paramSchema: '[]',
|
|
1648
|
-
variants:
|
|
1649
|
+
variants:
|
|
1650
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
1649
1651
|
});
|
|
1650
1652
|
const manifest = {
|
|
1651
1653
|
schemaVersion: '1.0.0',
|
|
@@ -2359,10 +2361,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2359
2361
|
});
|
|
2360
2362
|
});
|
|
2361
2363
|
|
|
2362
|
-
// ─── Assertion (d): assembleMaterialWithSkylightEntries
|
|
2364
|
+
// ─── Assertion (d): assembleMaterialWithSkylightEntries adds the engine-owned pair ───
|
|
2363
2365
|
|
|
2364
2366
|
describe('t40 round-4 (d) assembleMaterialWithSkylightEntries shape', () => {
|
|
2365
|
-
it('returns
|
|
2367
|
+
it('returns 16 BindGroupEntry values; entry 7..13 reference the skylight resources', () => {
|
|
2366
2368
|
const materialEntries = makeMaterialBindGroupEntries();
|
|
2367
2369
|
const irrView = { __brand: 'TextureView', id: 1 } as unknown as TextureView;
|
|
2368
2370
|
const irrSampler = { __brand: 'Sampler', id: 1 } as unknown as Sampler;
|
|
@@ -2382,7 +2384,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2382
2384
|
intensityBuffer: intensityBuf,
|
|
2383
2385
|
});
|
|
2384
2386
|
|
|
2385
|
-
expect(merged).toHaveLength(
|
|
2387
|
+
expect(merged).toHaveLength(16);
|
|
2386
2388
|
|
|
2387
2389
|
// Skylight entries at binding 7..13 in D-5 order
|
|
2388
2390
|
expect(merged[7]?.binding).toBe(7);
|
|
@@ -2402,6 +2404,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2402
2404
|
kind: 'buffer',
|
|
2403
2405
|
value: { buffer: intensityBuf },
|
|
2404
2406
|
});
|
|
2407
|
+
expect(merged[14]?.binding).toBe(14);
|
|
2408
|
+
expect(merged[15]?.binding).toBe(15);
|
|
2405
2409
|
});
|
|
2406
2410
|
|
|
2407
2411
|
it('derives IBL injection start from materialEntries.length (per-shader user-region, not a fixed 7)', () => {
|
|
@@ -2418,9 +2422,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2418
2422
|
brdfLutSampler: {} as Sampler,
|
|
2419
2423
|
intensityBuffer: {} as Buffer,
|
|
2420
2424
|
});
|
|
2421
|
-
expect(merged).toHaveLength(
|
|
2425
|
+
expect(merged).toHaveLength(9);
|
|
2422
2426
|
expect(merged[0]?.binding).toBe(0);
|
|
2423
2427
|
expect(merged[6]?.binding).toBe(6);
|
|
2428
|
+
expect(merged[7]?.binding).toBe(7);
|
|
2429
|
+
expect(merged[8]?.binding).toBe(8);
|
|
2424
2430
|
});
|
|
2425
2431
|
});
|
|
2426
2432
|
}
|
|
@@ -2515,7 +2521,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2515
2521
|
}
|
|
2516
2522
|
|
|
2517
2523
|
describe('t58 (M4 round-4) -- material BG fallback path', () => {
|
|
2518
|
-
it('(a) assemble with fallback returns
|
|
2524
|
+
it('(a) assemble with fallback returns 16 entries; binding 7..13 reference fallback resources', () => {
|
|
2519
2525
|
const materialEntries = makeMaterialBindGroupEntries();
|
|
2520
2526
|
const fallback = makeFallback();
|
|
2521
2527
|
const fallbackAsActive: SkylightBindGroupResources = {
|
|
@@ -2528,7 +2534,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2528
2534
|
intensityBuffer: fallback.intensityBuffer,
|
|
2529
2535
|
};
|
|
2530
2536
|
const merged = assembleMaterialWithSkylightEntries(materialEntries, fallbackAsActive);
|
|
2531
|
-
expect(merged).toHaveLength(
|
|
2537
|
+
expect(merged).toHaveLength(16);
|
|
2532
2538
|
expect(merged[7]?.binding).toBe(7);
|
|
2533
2539
|
expect((merged[7]?.resource as { value: unknown }).value).toBe(fallback.irradianceView);
|
|
2534
2540
|
expect((merged[8]?.resource as { value: unknown }).value).toBe(fallback.sampler);
|
|
@@ -2541,11 +2547,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2541
2547
|
);
|
|
2542
2548
|
});
|
|
2543
2549
|
|
|
2544
|
-
it('(b) assemble with active returns
|
|
2550
|
+
it('(b) assemble with active returns 16 entries; binding 7..13 reference active IblPipelineCache resources', () => {
|
|
2545
2551
|
const materialEntries = makeMaterialBindGroupEntries();
|
|
2546
2552
|
const active = makeActive();
|
|
2547
2553
|
const merged = assembleMaterialWithSkylightEntries(materialEntries, active);
|
|
2548
|
-
expect(merged).toHaveLength(
|
|
2554
|
+
expect(merged).toHaveLength(16);
|
|
2549
2555
|
expect((merged[7]?.resource as { value: unknown }).value).toBe(active.irradianceView);
|
|
2550
2556
|
expect((merged[8]?.resource as { value: unknown }).value).toBe(active.irradianceSampler);
|
|
2551
2557
|
expect((merged[9]?.resource as { value: unknown }).value).toBe(active.prefilterView);
|
|
@@ -2754,47 +2760,43 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2754
2760
|
expect(layout?.label).toBe('pbr-pl');
|
|
2755
2761
|
});
|
|
2756
2762
|
|
|
2757
|
-
it('(b) PBR material BGL entry count ===
|
|
2763
|
+
it('(b) PBR material BGL entry count === 26 (user region 0..16 + Skylight 17..23 + transmission 24..25)', () => {
|
|
2758
2764
|
const device = makeMockDevice();
|
|
2759
2765
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
2760
2766
|
buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
|
|
2761
2767
|
const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
|
|
2762
2768
|
expect(materialBgl).toBeDefined();
|
|
2763
|
-
expect(materialBgl?.entries).toHaveLength(
|
|
2769
|
+
expect(materialBgl?.entries).toHaveLength(26);
|
|
2764
2770
|
});
|
|
2765
2771
|
|
|
2766
|
-
it('(c) binding indices 0..
|
|
2772
|
+
it('(c) binding indices 0..25 in order; 17..23 resource types in D-5 round-4 order; 24..25 transmission', () => {
|
|
2767
2773
|
const device = makeMockDevice();
|
|
2768
2774
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
2769
2775
|
buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
|
|
2770
2776
|
const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
|
|
2771
2777
|
expect(materialBgl).toBeDefined();
|
|
2772
2778
|
const entries = materialBgl?.entries ?? [];
|
|
2773
|
-
// binding indices 0..
|
|
2774
|
-
for (let i = 0; i <
|
|
2779
|
+
// binding indices 0..25 in order
|
|
2780
|
+
for (let i = 0; i < 26; i++) {
|
|
2775
2781
|
expect(entries[i]?.binding).toBe(i);
|
|
2776
2782
|
}
|
|
2777
|
-
//
|
|
2778
|
-
expect((entries[
|
|
2783
|
+
// 17..23 resource types
|
|
2784
|
+
expect((entries[17] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2779
2785
|
'cube',
|
|
2780
2786
|
);
|
|
2781
|
-
expect((entries[
|
|
2782
|
-
expect((entries[
|
|
2787
|
+
expect((entries[18] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2788
|
+
expect((entries[19] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2783
2789
|
'cube',
|
|
2784
2790
|
);
|
|
2785
|
-
expect((entries[16] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2786
|
-
expect((entries[17] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2787
|
-
'2d',
|
|
2788
|
-
);
|
|
2789
|
-
expect((entries[18] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2790
|
-
expect((entries[19] as { buffer?: { type: string } }).buffer?.type).toBe('uniform');
|
|
2791
|
-
// 20..23 lightmap resource types
|
|
2792
2791
|
expect((entries[20] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2793
2792
|
expect((entries[21] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2794
2793
|
'2d',
|
|
2795
2794
|
);
|
|
2796
2795
|
expect((entries[22] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2797
|
-
expect((entries[23] as {
|
|
2796
|
+
expect((entries[23] as { buffer?: { type: string } }).buffer?.type).toBe('uniform');
|
|
2797
|
+
// 24..25 transmission resource types
|
|
2798
|
+
expect((entries[24] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2799
|
+
expect((entries[25] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2798
2800
|
'2d',
|
|
2799
2801
|
);
|
|
2800
2802
|
});
|
|
@@ -2820,13 +2822,13 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2820
2822
|
// ─── (e)(f) Unlit material BGL shape + name ─────────────────────────────────
|
|
2821
2823
|
|
|
2822
2824
|
describe('t57 (M4 round-4) -- unlitPipeline material BGL shape', () => {
|
|
2823
|
-
it('(e) unlit material BGL entry count ===
|
|
2825
|
+
it('(e) unlit material BGL entry count === 17 (no Skylight binding contamination)', () => {
|
|
2824
2826
|
const device = makeMockDevice();
|
|
2825
2827
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
2826
2828
|
buildUnlitMaterialBgl(device as any);
|
|
2827
2829
|
const unlitBgl = device.capturedBgls.find((b) => b.label === 'unlit-material-bgl');
|
|
2828
2830
|
expect(unlitBgl).toBeDefined();
|
|
2829
|
-
expect(unlitBgl?.entries).toHaveLength(
|
|
2831
|
+
expect(unlitBgl?.entries).toHaveLength(17);
|
|
2830
2832
|
});
|
|
2831
2833
|
|
|
2832
2834
|
it("(f) PBR material BGL labelled 'pbr-material-skylight-bgl'; unlit labelled 'unlit-material-bgl'", () => {
|
|
@@ -2986,6 +2988,14 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2986
2988
|
}
|
|
2987
2989
|
|
|
2988
2990
|
function buildManifestDataUrl(): string {
|
|
2991
|
+
const materialShaderStub = (identifier: string) => ({
|
|
2992
|
+
identifier,
|
|
2993
|
+
sourcePath: `${identifier}.wgsl`,
|
|
2994
|
+
composedWgsl: '/* stub */',
|
|
2995
|
+
paramSchema: '[]',
|
|
2996
|
+
variants:
|
|
2997
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
2998
|
+
});
|
|
2989
2999
|
const manifest = {
|
|
2990
3000
|
schemaVersion: '1.0.0',
|
|
2991
3001
|
entries: [
|
|
@@ -3008,6 +3018,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3008
3018
|
bindings: '',
|
|
3009
3019
|
},
|
|
3010
3020
|
],
|
|
3021
|
+
materialShaders: [
|
|
3022
|
+
materialShaderStub('forgeax::default-standard-pbr'),
|
|
3023
|
+
materialShaderStub('forgeax::default-unlit'),
|
|
3024
|
+
],
|
|
3011
3025
|
};
|
|
3012
3026
|
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
3013
3027
|
}
|
|
@@ -7292,7 +7306,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
7292
7306
|
sourcePath: `${identifier}.wgsl`,
|
|
7293
7307
|
composedWgsl,
|
|
7294
7308
|
paramSchema: '[]',
|
|
7295
|
-
variants:
|
|
7309
|
+
variants:
|
|
7310
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
7296
7311
|
});
|
|
7297
7312
|
const manifest = {
|
|
7298
7313
|
schemaVersion: '1.0.0',
|
|
@@ -7608,7 +7623,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
7608
7623
|
sourcePath: `${identifier}.wgsl`,
|
|
7609
7624
|
composedWgsl: '/* stub */',
|
|
7610
7625
|
paramSchema: '[]',
|
|
7611
|
-
variants:
|
|
7626
|
+
variants:
|
|
7627
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
7612
7628
|
});
|
|
7613
7629
|
const manifest = {
|
|
7614
7630
|
schemaVersion: '1.0.0',
|
|
@@ -8693,7 +8709,8 @@ type ExtractFramesWithPipeline = (
|
|
|
8693
8709
|
sourcePath: `${identifier}.wgsl`,
|
|
8694
8710
|
composedWgsl: '/* stub */',
|
|
8695
8711
|
paramSchema: '[]',
|
|
8696
|
-
variants:
|
|
8712
|
+
variants:
|
|
8713
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
8697
8714
|
});
|
|
8698
8715
|
const entries: Array<{ hash: string; wgsl: string; glsl: string; bindings: string }> = [
|
|
8699
8716
|
{ hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
|
package/src/backend-selection.ts
CHANGED
|
@@ -11,6 +11,9 @@ export function loadRhiPack(
|
|
|
11
11
|
const createShaderModule = mod.createShaderModule as
|
|
12
12
|
| RhiBackendPack['createShaderModule']
|
|
13
13
|
| undefined;
|
|
14
|
+
const createShaderModuleImmediate = mod.createShaderModuleImmediate as
|
|
15
|
+
| RhiBackendPack['createShaderModuleImmediate']
|
|
16
|
+
| undefined;
|
|
14
17
|
const translateErrorEventToRhiError = mod.translateErrorEventToRhiError as
|
|
15
18
|
| RhiBackendPack['translateErrorEventToRhiError']
|
|
16
19
|
| undefined;
|
|
@@ -23,6 +26,7 @@ export function loadRhiPack(
|
|
|
23
26
|
const pack: RhiBackendPack = {
|
|
24
27
|
rhi,
|
|
25
28
|
...(createShaderModule === undefined ? {} : { createShaderModule }),
|
|
29
|
+
...(createShaderModuleImmediate === undefined ? {} : { createShaderModuleImmediate }),
|
|
26
30
|
...(translateErrorEventToRhiError === undefined ? {} : { translateErrorEventToRhiError }),
|
|
27
31
|
...(rawDeviceAccessor === undefined ? {} : { _internal_getRawDevice: rawDeviceAccessor }),
|
|
28
32
|
};
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
// proven to be a mount entity, components is left undefined (one-time
|
|
45
45
|
// normalization on first reload; fixed-point from second collect onward).
|
|
46
46
|
|
|
47
|
-
import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
48
47
|
import {
|
|
48
|
+
builtinMeshGuid,
|
|
49
49
|
resolveAssetHandle,
|
|
50
50
|
SceneCollectAssetGuidUnresolvedError,
|
|
51
51
|
SceneCollectEntityRefOutOfClosureError,
|
|
@@ -78,6 +78,11 @@ import type {
|
|
|
78
78
|
} from '@forgeax/engine-types';
|
|
79
79
|
import { foldMountOverrides } from './scene-utils/mount-override-fold';
|
|
80
80
|
|
|
81
|
+
/** The collector needs identity lookup, not an authoring registry implementation. */
|
|
82
|
+
export interface SceneAssetGuidLookup {
|
|
83
|
+
guidOf(asset: Asset): string | undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
// Shared helpers
|
|
82
87
|
function _isArrayLike(value: unknown): value is ArrayLike<unknown> {
|
|
83
88
|
// SceneAsset values are ordinary JSON arrays at the boundary. Use the
|
|
@@ -116,14 +121,16 @@ function classifyFieldSchema(fieldType: string | undefined): SchemaFieldClass |
|
|
|
116
121
|
// lives in exactly one place; `field` names the failing field in the error.
|
|
117
122
|
function _handleToGuid(
|
|
118
123
|
world: World,
|
|
119
|
-
registry:
|
|
124
|
+
registry: SceneAssetGuidLookup,
|
|
120
125
|
handle: number,
|
|
121
126
|
field: string,
|
|
122
127
|
): Result<string | undefined, SceneCollectAssetGuidUnresolvedError> {
|
|
123
128
|
if (handle === 0) return ok(undefined); // NULL sentinel
|
|
129
|
+
const builtinGuid = builtinMeshGuid(handle as unknown as Handle<string, 'shared'>);
|
|
130
|
+
if (builtinGuid !== undefined) return ok(builtinGuid);
|
|
124
131
|
const assetRes = resolveAssetHandle(world, handle as unknown as Handle<string, 'shared'>);
|
|
125
132
|
if (!assetRes.ok) return err(new SceneCollectAssetGuidUnresolvedError(field, handle));
|
|
126
|
-
const guid = registry.
|
|
133
|
+
const guid = registry.guidOf(assetRes.value as Asset);
|
|
127
134
|
if (guid === undefined) return err(new SceneCollectAssetGuidUnresolvedError(field, handle));
|
|
128
135
|
return ok(guid);
|
|
129
136
|
}
|
|
@@ -135,7 +142,7 @@ function _handleToGuid(
|
|
|
135
142
|
// values pass through untouched.
|
|
136
143
|
function _serializeSharedFieldValue(
|
|
137
144
|
world: World,
|
|
138
|
-
registry:
|
|
145
|
+
registry: SceneAssetGuidLookup,
|
|
139
146
|
classification: SchemaFieldClass,
|
|
140
147
|
value: unknown,
|
|
141
148
|
field: string,
|
|
@@ -166,7 +173,7 @@ function _serializeSharedFieldValue(
|
|
|
166
173
|
// override IS that field so it cannot be omitted). Non-shared fields pass through.
|
|
167
174
|
function _serializeOverrideValueHandles(
|
|
168
175
|
world: World,
|
|
169
|
-
registry:
|
|
176
|
+
registry: SceneAssetGuidLookup,
|
|
170
177
|
ov: MountOverride,
|
|
171
178
|
): Result<unknown, SceneCollectAssetGuidUnresolvedError> {
|
|
172
179
|
const comp = world.components.resolve(ov.comp);
|
|
@@ -253,7 +260,7 @@ export function serializeSceneAssetToPack(
|
|
|
253
260
|
* filter (not subtree pruning): graft entities under members survive as owned.
|
|
254
261
|
*/
|
|
255
262
|
export function rootsToSceneAsset(
|
|
256
|
-
registry:
|
|
263
|
+
registry: SceneAssetGuidLookup,
|
|
257
264
|
world: World,
|
|
258
265
|
roots: EntityHandle[],
|
|
259
266
|
): Result<
|
|
@@ -415,7 +422,7 @@ export function rootsToSceneAsset(
|
|
|
415
422
|
sh.value as unknown as number,
|
|
416
423
|
),
|
|
417
424
|
);
|
|
418
|
-
const g = registry.
|
|
425
|
+
const g = registry.guidOf(pr.value as Asset);
|
|
419
426
|
if (g === undefined)
|
|
420
427
|
return err(
|
|
421
428
|
new SceneCollectAssetGuidUnresolvedError(
|
package/src/index.ts
CHANGED
|
@@ -326,7 +326,11 @@ export { quat } from '@forgeax/engine-math';
|
|
|
326
326
|
// collectSceneAsset removed — replaced by rootsToSceneAsset (forest entry,
|
|
327
327
|
// schema-derived field dispatch, engine-self-contained GUID resolution).
|
|
328
328
|
// serializeSceneAssetToPack now uses schema-derived refs[] index (D-1/D-2).
|
|
329
|
-
export {
|
|
329
|
+
export {
|
|
330
|
+
rootsToSceneAsset,
|
|
331
|
+
type SceneAssetGuidLookup,
|
|
332
|
+
serializeSceneAssetToPack,
|
|
333
|
+
} from './collect-scene-asset';
|
|
330
334
|
// feat-20260626 M6 / m6-4: debug-draw auto-attach glue is re-exported from the
|
|
331
335
|
// main barrel (was a separate tsup entry). The separate entry produced a SECOND
|
|
332
336
|
// module copy of the mutable `registeredDebugDraw` registry: createApp set it on
|
package/src/renderer-host.ts
CHANGED
|
@@ -4,7 +4,10 @@ import type {
|
|
|
4
4
|
BundlerOptions,
|
|
5
5
|
RendererHostAssembly,
|
|
6
6
|
} from '@forgeax/engine-render/internal/construct-renderer';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
constructRendererHost,
|
|
9
|
+
EngineEnvironmentError,
|
|
10
|
+
} from '@forgeax/engine-render/internal/construct-renderer';
|
|
8
11
|
import { loadBackendPack } from './backend-selection';
|
|
9
12
|
|
|
10
13
|
export type {
|
|
@@ -12,16 +15,40 @@ export type {
|
|
|
12
15
|
RendererHostAssembly,
|
|
13
16
|
} from '@forgeax/engine-render/internal/construct-renderer';
|
|
14
17
|
|
|
18
|
+
const FALLBACK_ERROR_CODES = new Set([
|
|
19
|
+
'adapter-unavailable',
|
|
20
|
+
'request-adapter-threw',
|
|
21
|
+
'feature-not-enabled',
|
|
22
|
+
'limit-exceeded',
|
|
23
|
+
'rhi-not-available',
|
|
24
|
+
'device-lost',
|
|
25
|
+
'oom',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function canFallbackToWgpu(error: unknown): boolean {
|
|
29
|
+
if (!(error instanceof EngineEnvironmentError)) return false;
|
|
30
|
+
const webgpuError = error.detail.webgpuError;
|
|
31
|
+
if (webgpuError === undefined || typeof webgpuError !== 'object') return false;
|
|
32
|
+
if (!('code' in webgpuError)) return false;
|
|
33
|
+
const code = webgpuError.code;
|
|
34
|
+
return typeof code === 'string' && FALLBACK_ERROR_CODES.has(code);
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
/** Assemble renderer plus asset services in the Runtime-owned backend host. */
|
|
16
38
|
export async function constructRuntimeRendererHost(
|
|
17
39
|
canvas: unknown,
|
|
18
40
|
options?: RendererOptions,
|
|
19
41
|
bundler?: BundlerOptions,
|
|
20
|
-
): Promise<RenderResult<RendererHostAssembly, RenderError>> {
|
|
42
|
+
): Promise<RenderResult<RendererHostAssembly, RenderError | EngineEnvironmentError>> {
|
|
21
43
|
const first = await loadBackendPack(options);
|
|
22
44
|
if (!first.ok) throw first.error;
|
|
23
45
|
const constructed = await constructRendererHost(canvas, options, bundler, first.value);
|
|
24
|
-
if (
|
|
46
|
+
if (
|
|
47
|
+
constructed.ok ||
|
|
48
|
+
options?.rhi !== undefined ||
|
|
49
|
+
typeof globalThis === 'undefined' ||
|
|
50
|
+
!canFallbackToWgpu(constructed.error)
|
|
51
|
+
) {
|
|
25
52
|
return constructed;
|
|
26
53
|
}
|
|
27
54
|
const fallback = await loadBackendPack(options, true);
|