@forgeax/engine-runtime 0.1.7 → 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.
Files changed (49) hide show
  1. package/README.md +45 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/helpers/standard-material-manifest.d.ts +12 -0
  4. package/dist/__tests__/helpers/standard-material-manifest.d.ts.map +1 -0
  5. package/dist/__tests__/render-environment-consumer.integration.test.d.ts +2 -0
  6. package/dist/__tests__/render-environment-consumer.integration.test.d.ts.map +1 -0
  7. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
  8. package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts.map +1 -1
  9. package/dist/__tests__/renderer-host-fallback.unit.test.d.ts +2 -0
  10. package/dist/__tests__/renderer-host-fallback.unit.test.d.ts.map +1 -0
  11. package/dist/__tests__/skinned-shadow-mixed.dawn.test.d.ts +2 -0
  12. package/dist/__tests__/skinned-shadow-mixed.dawn.test.d.ts.map +1 -0
  13. package/dist/backend-selection.d.ts.map +1 -1
  14. package/dist/index.mjs +20 -1
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/renderer-host.d.ts +2 -1
  17. package/dist/renderer-host.d.ts.map +1 -1
  18. package/dist/renderer-host.mjs +21 -2
  19. package/dist/renderer-host.mjs.map +1 -1
  20. package/package.json +30 -30
  21. package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +12 -11
  22. package/src/__tests__/errors.unit.test.ts +36 -0
  23. package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +4 -5
  24. package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +461 -18
  25. package/src/__tests__/geometry.unit.test.ts +3 -1
  26. package/src/__tests__/helpers/standard-material-manifest.ts +45 -0
  27. package/src/__tests__/material-texture-uv-scale.unit.test.ts +8 -14
  28. package/src/__tests__/materials.unit.test.ts +2 -0
  29. package/src/__tests__/pbr-pipeline.unit.test.ts +8 -8
  30. package/src/__tests__/pipeline-cache-keying.unit.test.ts +6 -4
  31. package/src/__tests__/pipeline.unit.test.ts +5 -2
  32. package/src/__tests__/render-environment-consumer.integration.test.ts +33 -0
  33. package/src/__tests__/render-error-exhaustive.test-d.ts +57 -0
  34. package/src/__tests__/render-feature-prepared-graphics.fixture.ts +10 -0
  35. package/src/__tests__/render-system-mega.test.ts +3 -1
  36. package/src/__tests__/render-system-record-multi-material-textureview.test.ts +3 -1
  37. package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +3 -1
  38. package/src/__tests__/render-system-record.test.ts +8 -6
  39. package/src/__tests__/renderer-host-fallback.unit.test.ts +112 -0
  40. package/src/__tests__/renderer-lifecycle.integration.test.ts +10 -0
  41. package/src/__tests__/renderer-surface.unit.test.ts +66 -0
  42. package/src/__tests__/shadow-csm-cascade-loadop.test.ts +5 -1
  43. package/src/__tests__/shadow-csm-tile-consistency.test.ts +5 -1
  44. package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +383 -0
  45. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +7 -7
  46. package/src/__tests__/ssao-passes.test.ts +28 -12
  47. package/src/__tests__/systems.unit.test.ts +48 -31
  48. package/src/backend-selection.ts +4 -0
  49. package/src/renderer-host.ts +30 -3
@@ -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 merges 7 + 7 = 14 ───
2364
+ // ─── Assertion (d): assembleMaterialWithSkylightEntries adds the engine-owned pair ───
2363
2365
 
2364
2366
  describe('t40 round-4 (d) assembleMaterialWithSkylightEntries shape', () => {
2365
- it('returns 14 BindGroupEntry values; entry 7..13 reference the skylight resources', () => {
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(14);
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(7);
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 14 entries; binding 7..13 reference fallback resources', () => {
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(14);
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 14 entries; binding 7..13 reference active IblPipelineCache resources', () => {
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(14);
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 === 24 (user region 0..12 + Skylight 13..19 + lightmap 20..23)', () => {
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(24);
2769
+ expect(materialBgl?.entries).toHaveLength(26);
2764
2770
  });
2765
2771
 
2766
- it('(c) binding indices 0..23 in order; 13..19 resource types in D-5 round-4 order; 20..23 lightmap', () => {
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..23 in order
2774
- for (let i = 0; i < 24; 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
- // 13..19 resource types
2778
- expect((entries[13] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
2783
+ // 17..23 resource types
2784
+ expect((entries[17] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
2779
2785
  'cube',
2780
2786
  );
2781
- expect((entries[14] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
2782
- expect((entries[15] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
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 { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
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 === 13 (no Skylight binding contamination)', () => {
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(13);
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: '' },
@@ -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
  };
@@ -4,7 +4,10 @@ import type {
4
4
  BundlerOptions,
5
5
  RendererHostAssembly,
6
6
  } from '@forgeax/engine-render/internal/construct-renderer';
7
- import { constructRendererHost } from '@forgeax/engine-render/internal/construct-renderer';
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 (constructed.ok || options?.rhi !== undefined || typeof globalThis === 'undefined') {
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);