@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
|
@@ -120,6 +120,8 @@ 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 { createTemporalFrameTransaction } from '../../../render/src/temporal/frame';
|
|
124
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
123
125
|
import { drawWithOwners } from './renderer-test-utils';
|
|
124
126
|
|
|
125
127
|
function componentFieldType(component: { fields: Record<string, { type: string }> }, name: string) {
|
|
@@ -761,6 +763,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
761
763
|
[],
|
|
762
764
|
{
|
|
763
765
|
frameNumber: 0,
|
|
766
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
767
|
+
temporalFrameInput: undefined,
|
|
764
768
|
compiledFrameGraph: null,
|
|
765
769
|
compiledFrameGraphTopologyKey: null,
|
|
766
770
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -816,6 +820,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
816
820
|
[],
|
|
817
821
|
{
|
|
818
822
|
frameNumber: 0,
|
|
823
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
824
|
+
temporalFrameInput: undefined,
|
|
819
825
|
compiledFrameGraph: null,
|
|
820
826
|
compiledFrameGraphTopologyKey: null,
|
|
821
827
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -1645,7 +1651,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1645
1651
|
sourcePath: `${identifier}.wgsl`,
|
|
1646
1652
|
composedWgsl,
|
|
1647
1653
|
paramSchema: '[]',
|
|
1648
|
-
variants:
|
|
1654
|
+
variants:
|
|
1655
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
1649
1656
|
});
|
|
1650
1657
|
const manifest = {
|
|
1651
1658
|
schemaVersion: '1.0.0',
|
|
@@ -2079,8 +2086,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2079
2086
|
);
|
|
2080
2087
|
});
|
|
2081
2088
|
|
|
2082
|
-
it('imports View from forgeax_view::common', () => {
|
|
2083
|
-
expect(source).toEqual(expect.stringContaining('#import forgeax_view::common::
|
|
2089
|
+
it('imports View from forgeax_view::common as the sole view ABI', () => {
|
|
2090
|
+
expect(source).toEqual(expect.stringContaining('#import forgeax_view::common::View'));
|
|
2091
|
+
expect(source).not.toContain('FogRay');
|
|
2092
|
+
expect(source).not.toContain('fogRayFromNdc');
|
|
2084
2093
|
});
|
|
2085
2094
|
|
|
2086
2095
|
it('imports fullscreen_triangle from forgeax_view::common', () => {
|
|
@@ -2359,10 +2368,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2359
2368
|
});
|
|
2360
2369
|
});
|
|
2361
2370
|
|
|
2362
|
-
// ─── Assertion (d): assembleMaterialWithSkylightEntries
|
|
2371
|
+
// ─── Assertion (d): assembleMaterialWithSkylightEntries adds the engine-owned pair ───
|
|
2363
2372
|
|
|
2364
2373
|
describe('t40 round-4 (d) assembleMaterialWithSkylightEntries shape', () => {
|
|
2365
|
-
it('returns
|
|
2374
|
+
it('returns 16 BindGroupEntry values; entry 7..13 reference the skylight resources', () => {
|
|
2366
2375
|
const materialEntries = makeMaterialBindGroupEntries();
|
|
2367
2376
|
const irrView = { __brand: 'TextureView', id: 1 } as unknown as TextureView;
|
|
2368
2377
|
const irrSampler = { __brand: 'Sampler', id: 1 } as unknown as Sampler;
|
|
@@ -2382,7 +2391,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2382
2391
|
intensityBuffer: intensityBuf,
|
|
2383
2392
|
});
|
|
2384
2393
|
|
|
2385
|
-
expect(merged).toHaveLength(
|
|
2394
|
+
expect(merged).toHaveLength(16);
|
|
2386
2395
|
|
|
2387
2396
|
// Skylight entries at binding 7..13 in D-5 order
|
|
2388
2397
|
expect(merged[7]?.binding).toBe(7);
|
|
@@ -2402,6 +2411,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2402
2411
|
kind: 'buffer',
|
|
2403
2412
|
value: { buffer: intensityBuf },
|
|
2404
2413
|
});
|
|
2414
|
+
expect(merged[14]?.binding).toBe(14);
|
|
2415
|
+
expect(merged[15]?.binding).toBe(15);
|
|
2405
2416
|
});
|
|
2406
2417
|
|
|
2407
2418
|
it('derives IBL injection start from materialEntries.length (per-shader user-region, not a fixed 7)', () => {
|
|
@@ -2418,9 +2429,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2418
2429
|
brdfLutSampler: {} as Sampler,
|
|
2419
2430
|
intensityBuffer: {} as Buffer,
|
|
2420
2431
|
});
|
|
2421
|
-
expect(merged).toHaveLength(
|
|
2432
|
+
expect(merged).toHaveLength(9);
|
|
2422
2433
|
expect(merged[0]?.binding).toBe(0);
|
|
2423
2434
|
expect(merged[6]?.binding).toBe(6);
|
|
2435
|
+
expect(merged[7]?.binding).toBe(7);
|
|
2436
|
+
expect(merged[8]?.binding).toBe(8);
|
|
2424
2437
|
});
|
|
2425
2438
|
});
|
|
2426
2439
|
}
|
|
@@ -2515,7 +2528,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2515
2528
|
}
|
|
2516
2529
|
|
|
2517
2530
|
describe('t58 (M4 round-4) -- material BG fallback path', () => {
|
|
2518
|
-
it('(a) assemble with fallback returns
|
|
2531
|
+
it('(a) assemble with fallback returns 16 entries; binding 7..13 reference fallback resources', () => {
|
|
2519
2532
|
const materialEntries = makeMaterialBindGroupEntries();
|
|
2520
2533
|
const fallback = makeFallback();
|
|
2521
2534
|
const fallbackAsActive: SkylightBindGroupResources = {
|
|
@@ -2528,7 +2541,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2528
2541
|
intensityBuffer: fallback.intensityBuffer,
|
|
2529
2542
|
};
|
|
2530
2543
|
const merged = assembleMaterialWithSkylightEntries(materialEntries, fallbackAsActive);
|
|
2531
|
-
expect(merged).toHaveLength(
|
|
2544
|
+
expect(merged).toHaveLength(16);
|
|
2532
2545
|
expect(merged[7]?.binding).toBe(7);
|
|
2533
2546
|
expect((merged[7]?.resource as { value: unknown }).value).toBe(fallback.irradianceView);
|
|
2534
2547
|
expect((merged[8]?.resource as { value: unknown }).value).toBe(fallback.sampler);
|
|
@@ -2541,11 +2554,11 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2541
2554
|
);
|
|
2542
2555
|
});
|
|
2543
2556
|
|
|
2544
|
-
it('(b) assemble with active returns
|
|
2557
|
+
it('(b) assemble with active returns 16 entries; binding 7..13 reference active IblPipelineCache resources', () => {
|
|
2545
2558
|
const materialEntries = makeMaterialBindGroupEntries();
|
|
2546
2559
|
const active = makeActive();
|
|
2547
2560
|
const merged = assembleMaterialWithSkylightEntries(materialEntries, active);
|
|
2548
|
-
expect(merged).toHaveLength(
|
|
2561
|
+
expect(merged).toHaveLength(16);
|
|
2549
2562
|
expect((merged[7]?.resource as { value: unknown }).value).toBe(active.irradianceView);
|
|
2550
2563
|
expect((merged[8]?.resource as { value: unknown }).value).toBe(active.irradianceSampler);
|
|
2551
2564
|
expect((merged[9]?.resource as { value: unknown }).value).toBe(active.prefilterView);
|
|
@@ -2754,47 +2767,43 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2754
2767
|
expect(layout?.label).toBe('pbr-pl');
|
|
2755
2768
|
});
|
|
2756
2769
|
|
|
2757
|
-
it('(b) PBR material BGL entry count ===
|
|
2770
|
+
it('(b) PBR material BGL entry count === 26 (user region 0..16 + Skylight 17..23 + transmission 24..25)', () => {
|
|
2758
2771
|
const device = makeMockDevice();
|
|
2759
2772
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
2760
2773
|
buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
|
|
2761
2774
|
const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
|
|
2762
2775
|
expect(materialBgl).toBeDefined();
|
|
2763
|
-
expect(materialBgl?.entries).toHaveLength(
|
|
2776
|
+
expect(materialBgl?.entries).toHaveLength(26);
|
|
2764
2777
|
});
|
|
2765
2778
|
|
|
2766
|
-
it('(c) binding indices 0..
|
|
2779
|
+
it('(c) binding indices 0..25 in order; 17..23 resource types in D-5 round-4 order; 24..25 transmission', () => {
|
|
2767
2780
|
const device = makeMockDevice();
|
|
2768
2781
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
2769
2782
|
buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
|
|
2770
2783
|
const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
|
|
2771
2784
|
expect(materialBgl).toBeDefined();
|
|
2772
2785
|
const entries = materialBgl?.entries ?? [];
|
|
2773
|
-
// binding indices 0..
|
|
2774
|
-
for (let i = 0; i <
|
|
2786
|
+
// binding indices 0..25 in order
|
|
2787
|
+
for (let i = 0; i < 26; i++) {
|
|
2775
2788
|
expect(entries[i]?.binding).toBe(i);
|
|
2776
2789
|
}
|
|
2777
|
-
//
|
|
2778
|
-
expect((entries[
|
|
2790
|
+
// 17..23 resource types
|
|
2791
|
+
expect((entries[17] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2779
2792
|
'cube',
|
|
2780
2793
|
);
|
|
2781
|
-
expect((entries[
|
|
2782
|
-
expect((entries[
|
|
2794
|
+
expect((entries[18] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2795
|
+
expect((entries[19] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2783
2796
|
'cube',
|
|
2784
2797
|
);
|
|
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
2798
|
expect((entries[20] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2793
2799
|
expect((entries[21] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2794
2800
|
'2d',
|
|
2795
2801
|
);
|
|
2796
2802
|
expect((entries[22] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2797
|
-
expect((entries[23] as {
|
|
2803
|
+
expect((entries[23] as { buffer?: { type: string } }).buffer?.type).toBe('uniform');
|
|
2804
|
+
// 24..25 transmission resource types
|
|
2805
|
+
expect((entries[24] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2806
|
+
expect((entries[25] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2798
2807
|
'2d',
|
|
2799
2808
|
);
|
|
2800
2809
|
});
|
|
@@ -2820,13 +2829,13 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2820
2829
|
// ─── (e)(f) Unlit material BGL shape + name ─────────────────────────────────
|
|
2821
2830
|
|
|
2822
2831
|
describe('t57 (M4 round-4) -- unlitPipeline material BGL shape', () => {
|
|
2823
|
-
it('(e) unlit material BGL entry count ===
|
|
2832
|
+
it('(e) unlit material BGL entry count === 17 (no Skylight binding contamination)', () => {
|
|
2824
2833
|
const device = makeMockDevice();
|
|
2825
2834
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
2826
2835
|
buildUnlitMaterialBgl(device as any);
|
|
2827
2836
|
const unlitBgl = device.capturedBgls.find((b) => b.label === 'unlit-material-bgl');
|
|
2828
2837
|
expect(unlitBgl).toBeDefined();
|
|
2829
|
-
expect(unlitBgl?.entries).toHaveLength(
|
|
2838
|
+
expect(unlitBgl?.entries).toHaveLength(17);
|
|
2830
2839
|
});
|
|
2831
2840
|
|
|
2832
2841
|
it("(f) PBR material BGL labelled 'pbr-material-skylight-bgl'; unlit labelled 'unlit-material-bgl'", () => {
|
|
@@ -2986,6 +2995,14 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
2986
2995
|
}
|
|
2987
2996
|
|
|
2988
2997
|
function buildManifestDataUrl(): string {
|
|
2998
|
+
const materialShaderStub = (identifier: string) => ({
|
|
2999
|
+
identifier,
|
|
3000
|
+
sourcePath: `${identifier}.wgsl`,
|
|
3001
|
+
composedWgsl: '/* stub */',
|
|
3002
|
+
paramSchema: '[]',
|
|
3003
|
+
variants:
|
|
3004
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
3005
|
+
});
|
|
2989
3006
|
const manifest = {
|
|
2990
3007
|
schemaVersion: '1.0.0',
|
|
2991
3008
|
entries: [
|
|
@@ -3008,6 +3025,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3008
3025
|
bindings: '',
|
|
3009
3026
|
},
|
|
3010
3027
|
],
|
|
3028
|
+
materialShaders: [
|
|
3029
|
+
materialShaderStub('forgeax::default-standard-pbr'),
|
|
3030
|
+
materialShaderStub('forgeax::default-unlit'),
|
|
3031
|
+
],
|
|
3011
3032
|
};
|
|
3012
3033
|
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
3013
3034
|
}
|
|
@@ -3436,6 +3457,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3436
3457
|
[],
|
|
3437
3458
|
{
|
|
3438
3459
|
frameNumber: 0,
|
|
3460
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
3461
|
+
temporalFrameInput: undefined,
|
|
3439
3462
|
compiledFrameGraph: null,
|
|
3440
3463
|
compiledFrameGraphTopologyKey: null,
|
|
3441
3464
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -3555,6 +3578,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3555
3578
|
[],
|
|
3556
3579
|
{
|
|
3557
3580
|
frameNumber: 0,
|
|
3581
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
3582
|
+
temporalFrameInput: undefined,
|
|
3558
3583
|
compiledFrameGraph: null,
|
|
3559
3584
|
compiledFrameGraphTopologyKey: null,
|
|
3560
3585
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -3628,6 +3653,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3628
3653
|
[],
|
|
3629
3654
|
{
|
|
3630
3655
|
frameNumber: 0,
|
|
3656
|
+
temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
|
|
3657
|
+
temporalFrameInput: undefined,
|
|
3631
3658
|
compiledFrameGraph: null,
|
|
3632
3659
|
compiledFrameGraphTopologyKey: null,
|
|
3633
3660
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -7292,7 +7319,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
7292
7319
|
sourcePath: `${identifier}.wgsl`,
|
|
7293
7320
|
composedWgsl,
|
|
7294
7321
|
paramSchema: '[]',
|
|
7295
|
-
variants:
|
|
7322
|
+
variants:
|
|
7323
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
7296
7324
|
});
|
|
7297
7325
|
const manifest = {
|
|
7298
7326
|
schemaVersion: '1.0.0',
|
|
@@ -7608,7 +7636,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
7608
7636
|
sourcePath: `${identifier}.wgsl`,
|
|
7609
7637
|
composedWgsl: '/* stub */',
|
|
7610
7638
|
paramSchema: '[]',
|
|
7611
|
-
variants:
|
|
7639
|
+
variants:
|
|
7640
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
7612
7641
|
});
|
|
7613
7642
|
const manifest = {
|
|
7614
7643
|
schemaVersion: '1.0.0',
|
|
@@ -8693,7 +8722,8 @@ type ExtractFramesWithPipeline = (
|
|
|
8693
8722
|
sourcePath: `${identifier}.wgsl`,
|
|
8694
8723
|
composedWgsl: '/* stub */',
|
|
8695
8724
|
paramSchema: '[]',
|
|
8696
|
-
variants:
|
|
8725
|
+
variants:
|
|
8726
|
+
identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
|
|
8697
8727
|
});
|
|
8698
8728
|
const entries: Array<{ hash: string; wgsl: string; glsl: string; bindings: string }> = [
|
|
8699
8729
|
{ hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const TEXTURE_BINDING = 0x04;
|
|
4
|
+
const COPY_DST = 0x08;
|
|
5
|
+
const COPY_SRC = 0x01;
|
|
6
|
+
const STORAGE_BINDING = 0x80;
|
|
7
|
+
const MAP_READ = 0x0001;
|
|
8
|
+
const COMPUTE_STAGE = 1;
|
|
9
|
+
|
|
10
|
+
type ProbeResult =
|
|
11
|
+
| { readonly status: 'ready'; readonly samples: readonly (readonly number[])[] }
|
|
12
|
+
| { readonly status: 'unavailable'; readonly reason: string };
|
|
13
|
+
|
|
14
|
+
async function probe(): Promise<ProbeResult> {
|
|
15
|
+
if (!('gpu' in navigator)) return { status: 'unavailable', reason: 'webgpu-unavailable' };
|
|
16
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
17
|
+
if (adapter === null) return { status: 'unavailable', reason: 'adapter-unavailable' };
|
|
18
|
+
const device = await adapter.requestDevice();
|
|
19
|
+
const texture = device.createTexture({
|
|
20
|
+
size: { width: 2, height: 2, depthOrArrayLayers: 2 },
|
|
21
|
+
dimension: '3d',
|
|
22
|
+
format: 'rgba8unorm',
|
|
23
|
+
usage: TEXTURE_BINDING | COPY_DST,
|
|
24
|
+
});
|
|
25
|
+
const arrayTexture = device.createTexture({
|
|
26
|
+
size: { width: 2, height: 2, depthOrArrayLayers: 2 },
|
|
27
|
+
dimension: '2d',
|
|
28
|
+
format: 'rgba8unorm',
|
|
29
|
+
usage: TEXTURE_BINDING | COPY_DST,
|
|
30
|
+
});
|
|
31
|
+
const samples = new Uint8Array(256 * 2 * 2);
|
|
32
|
+
for (let point = 0; point < 8; point += 1) {
|
|
33
|
+
const z = Math.floor(point / 4);
|
|
34
|
+
const y = Math.floor((point % 4) / 2);
|
|
35
|
+
const x = point % 2;
|
|
36
|
+
const offset = z * 512 + y * 256 + x * 4;
|
|
37
|
+
samples[offset] = point + 1;
|
|
38
|
+
samples[offset + 3] = 255;
|
|
39
|
+
}
|
|
40
|
+
const layout = { offset: 0, bytesPerRow: 256, rowsPerImage: 2 };
|
|
41
|
+
const extent = { width: 2, height: 2, depthOrArrayLayers: 2 };
|
|
42
|
+
device.queue.writeTexture({ texture: arrayTexture }, samples, layout, extent);
|
|
43
|
+
device.queue.writeTexture({ texture }, samples, layout, extent);
|
|
44
|
+
const output = device.createBuffer({ size: 256, usage: STORAGE_BINDING | COPY_SRC });
|
|
45
|
+
const readback = device.createBuffer({ size: 256, usage: MAP_READ | COPY_DST });
|
|
46
|
+
const module = device.createShaderModule({
|
|
47
|
+
code: `
|
|
48
|
+
@group(0) @binding(0) var array_tex: texture_2d_array<f32>;
|
|
49
|
+
@group(0) @binding(1) var volume_tex: texture_3d<f32>;
|
|
50
|
+
@group(0) @binding(2) var<storage, read_write> output: array<vec4<u32>>;
|
|
51
|
+
fn to_byte(value: f32) -> u32 {
|
|
52
|
+
return u32(round(value * 255.0));
|
|
53
|
+
}
|
|
54
|
+
@compute @workgroup_size(1)
|
|
55
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
56
|
+
let point = id.x;
|
|
57
|
+
let coord = vec3<i32>(i32(point % 2u), i32((point / 2u) % 2u), i32(point / 4u));
|
|
58
|
+
let array_value = textureLoad(array_tex, coord.xy, coord.z, 0);
|
|
59
|
+
let volume_value = textureLoad(volume_tex, coord, 0);
|
|
60
|
+
output[point] = vec4<u32>(to_byte(array_value.r), 0u, 0u, 0u);
|
|
61
|
+
output[point + 8u] = vec4<u32>(to_byte(volume_value.r), 0u, 0u, 0u);
|
|
62
|
+
}
|
|
63
|
+
`,
|
|
64
|
+
});
|
|
65
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
66
|
+
entries: [
|
|
67
|
+
{ binding: 0, visibility: COMPUTE_STAGE, texture: { viewDimension: '2d-array' } },
|
|
68
|
+
{ binding: 1, visibility: COMPUTE_STAGE, texture: { viewDimension: '3d' } },
|
|
69
|
+
{
|
|
70
|
+
binding: 2,
|
|
71
|
+
visibility: COMPUTE_STAGE,
|
|
72
|
+
buffer: { type: 'storage' },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
const pipeline = device.createComputePipeline({
|
|
77
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
|
|
78
|
+
compute: { module, entryPoint: 'main' },
|
|
79
|
+
});
|
|
80
|
+
const bindGroup = device.createBindGroup({
|
|
81
|
+
layout: bindGroupLayout,
|
|
82
|
+
entries: [
|
|
83
|
+
{ binding: 0, resource: arrayTexture.createView({ dimension: '2d-array' }) },
|
|
84
|
+
{ binding: 1, resource: texture.createView({ dimension: '3d' }) },
|
|
85
|
+
{ binding: 2, resource: { buffer: output } },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const encoder = device.createCommandEncoder();
|
|
89
|
+
const pass = encoder.beginComputePass();
|
|
90
|
+
pass.setPipeline(pipeline);
|
|
91
|
+
pass.setBindGroup(0, bindGroup);
|
|
92
|
+
pass.dispatchWorkgroups(8);
|
|
93
|
+
pass.end();
|
|
94
|
+
encoder.copyBufferToBuffer(output, 0, readback, 0, 256);
|
|
95
|
+
device.queue.submit([encoder.finish()]);
|
|
96
|
+
await readback.mapAsync(MAP_READ);
|
|
97
|
+
const actual = new Uint8Array(readback.getMappedRange()).slice();
|
|
98
|
+
readback.unmap();
|
|
99
|
+
const observed = Array.from({ length: 8 }, (_, point) => [
|
|
100
|
+
actual[point * 16] ?? 0,
|
|
101
|
+
actual[(point + 8) * 16] ?? 0,
|
|
102
|
+
]);
|
|
103
|
+
const expected = Array.from({ length: 8 }, (_, point) => point + 1);
|
|
104
|
+
if (
|
|
105
|
+
observed.some(
|
|
106
|
+
([arraySample, volumeSample], point) =>
|
|
107
|
+
arraySample !== expected[point] || volumeSample !== expected[point],
|
|
108
|
+
)
|
|
109
|
+
) {
|
|
110
|
+
return { status: 'unavailable', reason: 'gpu-readback-mismatch' };
|
|
111
|
+
}
|
|
112
|
+
return { status: 'ready', samples: observed };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe('browser texture dimensions', () => {
|
|
116
|
+
it('returns an observable WebGPU status instead of silently accepting a bad view', async () => {
|
|
117
|
+
const result = await probe();
|
|
118
|
+
expect(['ready', 'unavailable']).toContain(result.status);
|
|
119
|
+
if (result.status === 'unavailable') expect(result.reason.length).toBeGreaterThan(0);
|
|
120
|
+
if (result.status === 'ready') {
|
|
121
|
+
expect(result.samples).toHaveLength(8);
|
|
122
|
+
expect(
|
|
123
|
+
result.samples.map(([arraySample, volumeSample]) => [arraySample, volumeSample]),
|
|
124
|
+
).toEqual(Array.from({ length: 8 }, (_, point) => [point + 1, point + 1]));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -126,7 +126,11 @@ function wireFetch(
|
|
|
126
126
|
{
|
|
127
127
|
guid: GUID_TEX,
|
|
128
128
|
kind: 'texture',
|
|
129
|
-
payload: {
|
|
129
|
+
payload: {
|
|
130
|
+
shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
|
|
131
|
+
colorSpace,
|
|
132
|
+
mips: { kind: 'none' },
|
|
133
|
+
},
|
|
130
134
|
refs: [],
|
|
131
135
|
artifacts: {
|
|
132
136
|
body: {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// @perf-budget-skip: intentional full Renderer submit-failure and LKG recovery integration gate.
|
|
2
|
+
import { World } from '@forgeax/engine-ecs';
|
|
3
|
+
import {
|
|
4
|
+
Camera,
|
|
5
|
+
DirectionalLight,
|
|
6
|
+
perspective,
|
|
7
|
+
TONEMAP_ACES_FILMIC,
|
|
8
|
+
VolumetricFog,
|
|
9
|
+
} from '@forgeax/engine-render';
|
|
10
|
+
import type { CommandBuffer, RhiQueue } from '@forgeax/engine-rhi';
|
|
11
|
+
import { RhiError } from '@forgeax/engine-rhi';
|
|
12
|
+
import { rhi as nullRhi, RhiNullAdapter } from '@forgeax/engine-rhi-null';
|
|
13
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
14
|
+
import { err, type Result, type TextureAsset } from '@forgeax/engine-types';
|
|
15
|
+
import { describe, expect, it } from 'vitest';
|
|
16
|
+
import { createRenderer } from '../createRenderer';
|
|
17
|
+
|
|
18
|
+
const VOLUME_MANIFEST = `data:application/json,${encodeURIComponent(
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
entries: [
|
|
21
|
+
{ hash: 'pbr', wgsl: 'f_schlick', glsl: '', bindings: '' },
|
|
22
|
+
{ hash: 'unlit', wgsl: 'unlit', glsl: '', bindings: '' },
|
|
23
|
+
{ hash: 'tonemap', wgsl: 'struct TonemapParams {}', glsl: '', bindings: '' },
|
|
24
|
+
{ hash: 'volume-inject', wgsl: 'fn volume_inject() {}', glsl: '', bindings: '' },
|
|
25
|
+
{ hash: 'volume-integrate', wgsl: 'fn volume_integrate() {}', glsl: '', bindings: '' },
|
|
26
|
+
{ hash: 'volume-temporal', wgsl: 'fn volume_temporal() {}', glsl: '', bindings: '' },
|
|
27
|
+
{ hash: 'volume-composite', wgsl: 'fn volume_fs() {}', glsl: '', bindings: '' },
|
|
28
|
+
],
|
|
29
|
+
}),
|
|
30
|
+
)}`;
|
|
31
|
+
|
|
32
|
+
function canvas(): HTMLCanvasElement {
|
|
33
|
+
return { width: 64, height: 64, getContext: () => null } as unknown as HTMLCanvasElement;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function density(): TextureAsset {
|
|
37
|
+
return {
|
|
38
|
+
kind: 'texture',
|
|
39
|
+
shape: { viewDimension: '3d', extent: { width: 64, height: 64, depth: 64 } },
|
|
40
|
+
format: 'r8unorm',
|
|
41
|
+
colorSpace: 'linear',
|
|
42
|
+
mips: { kind: 'none' },
|
|
43
|
+
data: new Uint8Array(64 * 64 * 64).fill(32),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function failingNullRhi(controller: { failNext: boolean }) {
|
|
48
|
+
const adapter = new RhiNullAdapter();
|
|
49
|
+
const requestDevice = adapter.requestDevice.bind(adapter);
|
|
50
|
+
adapter.requestDevice = async (options) => {
|
|
51
|
+
const result = await requestDevice(options);
|
|
52
|
+
if (!result.ok) return result;
|
|
53
|
+
const queue: RhiQueue = result.value.queue;
|
|
54
|
+
const submit = queue.submit.bind(queue);
|
|
55
|
+
queue.submit = (buffers: readonly CommandBuffer[]): Result<void, RhiError> => {
|
|
56
|
+
if (controller.failNext) {
|
|
57
|
+
controller.failNext = false;
|
|
58
|
+
return err(
|
|
59
|
+
new RhiError({
|
|
60
|
+
code: 'webgpu-runtime-error',
|
|
61
|
+
expected: 'queue submission to succeed',
|
|
62
|
+
hint: 'the recovery regression intentionally rejects one submit',
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return submit(buffers);
|
|
67
|
+
};
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
...nullRhi,
|
|
72
|
+
requestAdapter: async () => ({ ok: true, value: adapter }),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('volumetric fog record recovery projection', () => {
|
|
77
|
+
it('keeps accepted LKG inspection on a failed submit and clears it after repair', async () => {
|
|
78
|
+
const controller = { failNext: false };
|
|
79
|
+
const created = await createRenderer(
|
|
80
|
+
canvas(),
|
|
81
|
+
{ rhi: failingNullRhi(controller) as never },
|
|
82
|
+
{ shaderManifestUrl: VOLUME_MANIFEST },
|
|
83
|
+
);
|
|
84
|
+
expect(created.ok).toBe(true);
|
|
85
|
+
if (!created.ok) return;
|
|
86
|
+
const renderer = created.value;
|
|
87
|
+
|
|
88
|
+
const world = new World();
|
|
89
|
+
const densityHandle = world.allocSharedRef('TextureAsset', density());
|
|
90
|
+
world
|
|
91
|
+
.spawn(
|
|
92
|
+
{ component: Transform, data: { pos: [0, 0, 3] } },
|
|
93
|
+
{
|
|
94
|
+
component: Camera,
|
|
95
|
+
data: {
|
|
96
|
+
...perspective({ fov: Math.PI / 4, aspect: 1 }),
|
|
97
|
+
tonemap: TONEMAP_ACES_FILMIC,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
.unwrap();
|
|
102
|
+
const directionalLight = world
|
|
103
|
+
.spawn({
|
|
104
|
+
component: DirectionalLight,
|
|
105
|
+
data: { direction: [-0.4, -0.8, -0.3], castShadow: true },
|
|
106
|
+
})
|
|
107
|
+
.unwrap();
|
|
108
|
+
const fogEntity = world
|
|
109
|
+
.spawn({
|
|
110
|
+
component: VolumetricFog,
|
|
111
|
+
data: {
|
|
112
|
+
light: directionalLight,
|
|
113
|
+
density: densityHandle,
|
|
114
|
+
boundsMin: [-1, -1, -1],
|
|
115
|
+
boundsMax: [1, 1, 1],
|
|
116
|
+
extinction: [0.2, 0.2, 0.2],
|
|
117
|
+
albedo: [0.8, 0.8, 0.8],
|
|
118
|
+
emission: [0, 0, 0],
|
|
119
|
+
anisotropy: 0,
|
|
120
|
+
maxDistance: 50,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
.unwrap();
|
|
124
|
+
expect(world.update(1 / 60).ok).toBe(true);
|
|
125
|
+
|
|
126
|
+
const lease = renderer.attach(world);
|
|
127
|
+
expect(lease.ok).toBe(true);
|
|
128
|
+
if (!lease.ok) return;
|
|
129
|
+
const request = {
|
|
130
|
+
leases: [lease.value],
|
|
131
|
+
camera: { lease: lease.value },
|
|
132
|
+
environment: { lease: lease.value },
|
|
133
|
+
} as const;
|
|
134
|
+
|
|
135
|
+
expect(renderer.draw(request).ok).toBe(true);
|
|
136
|
+
expect(renderer.inspect().volumetricFog).toMatchObject({
|
|
137
|
+
status: 'available',
|
|
138
|
+
resourceStage: 'accepted',
|
|
139
|
+
generation: 1,
|
|
140
|
+
passCount: 4,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
controller.failNext = true;
|
|
144
|
+
expect(renderer.draw(request).ok).toBe(false);
|
|
145
|
+
expect(renderer.inspect().volumetricFog).toMatchObject({
|
|
146
|
+
status: 'degraded',
|
|
147
|
+
resourceStage: 'lkg',
|
|
148
|
+
generation: 1,
|
|
149
|
+
lkgGeneration: 1,
|
|
150
|
+
candidateGeneration: 1,
|
|
151
|
+
candidateFailure: 'submit-failed',
|
|
152
|
+
passCount: 4,
|
|
153
|
+
// The packed high profile uses a 64x64x16 physical froxel texture,
|
|
154
|
+
// representing 64 logical slices per pixel. Inspection reports the
|
|
155
|
+
// descriptor-derived allocation bytes for this recovery fixture.
|
|
156
|
+
sampleCount: 64 * 64 * 16,
|
|
157
|
+
memoryBytes: 71808,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(renderer.draw(request).ok).toBe(true);
|
|
161
|
+
expect(renderer.inspect().volumetricFog).toMatchObject({
|
|
162
|
+
status: 'available',
|
|
163
|
+
resourceStage: 'accepted',
|
|
164
|
+
generation: 1,
|
|
165
|
+
passCount: 4,
|
|
166
|
+
candidateFailure: undefined,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(world.removeComponent(fogEntity, VolumetricFog).ok).toBe(true);
|
|
170
|
+
expect(world.update(1 / 60).ok).toBe(true);
|
|
171
|
+
expect(renderer.draw(request).ok).toBe(true);
|
|
172
|
+
expect(renderer.inspect().volumetricFog).toMatchObject({
|
|
173
|
+
status: 'off',
|
|
174
|
+
passCount: 0,
|
|
175
|
+
sampleCount: 0,
|
|
176
|
+
memoryBytes: 0,
|
|
177
|
+
});
|
|
178
|
+
expect((await renderer.dispose()).ok).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
});
|
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
|
};
|
package/src/createRenderer.ts
CHANGED
|
@@ -30,6 +30,9 @@ export async function createRenderer(
|
|
|
30
30
|
...(options.rhi === undefined ? {} : { rhi: options.rhi }),
|
|
31
31
|
...(options.features === undefined ? {} : { features: options.features }),
|
|
32
32
|
...(options.profiler === undefined ? {} : { profiler: options.profiler }),
|
|
33
|
+
...(options.captureGpuTimings === undefined
|
|
34
|
+
? {}
|
|
35
|
+
: { captureGpuTimings: options.captureGpuTimings }),
|
|
33
36
|
...(options.rhiInstrumentation === undefined
|
|
34
37
|
? {}
|
|
35
38
|
: { rhiInstrumentation: options.rhiInstrumentation }),
|