@forgeax/engine-runtime 0.1.6 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-runtime",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,37 +26,37 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@forgeax/engine-animation": "0.1.6",
30
- "@forgeax/engine-assets-runtime": "0.1.6",
31
- "@forgeax/engine-audio-webaudio": "0.1.6",
32
- "@forgeax/engine-codec": "0.1.6",
33
- "@forgeax/engine-debug-draw": "0.1.6",
34
- "@forgeax/engine-ecs": "0.1.6",
35
- "@forgeax/engine-font": "0.1.6",
36
- "@forgeax/engine-geometry": "0.1.6",
37
- "@forgeax/engine-graphics-extras": "0.1.6",
38
- "@forgeax/engine-image": "0.1.6",
39
- "@forgeax/engine-input": "0.1.6",
40
- "@forgeax/engine-math": "0.1.6",
41
- "@forgeax/engine-pack": "0.1.6",
42
- "@forgeax/engine-profiler": "0.1.6",
43
- "@forgeax/engine-render": "0.1.6",
44
- "@forgeax/engine-render-graph": "0.1.6",
45
- "@forgeax/engine-rhi": "0.1.6",
46
- "@forgeax/engine-rhi-null": "0.1.6",
47
- "@forgeax/engine-rhi-webgpu": "0.1.6",
48
- "@forgeax/engine-rhi-wgpu": "0.1.6",
49
- "@forgeax/engine-scene": "0.1.6",
50
- "@forgeax/engine-shader": "0.1.6",
51
- "@forgeax/engine-skinning": "0.1.6",
52
- "@forgeax/engine-types": "0.1.6",
53
- "@forgeax/engine-vfx": "0.1.6"
29
+ "@forgeax/engine-animation": "0.1.7",
30
+ "@forgeax/engine-assets-runtime": "0.1.7",
31
+ "@forgeax/engine-audio-webaudio": "0.1.7",
32
+ "@forgeax/engine-codec": "0.1.7",
33
+ "@forgeax/engine-debug-draw": "0.1.7",
34
+ "@forgeax/engine-ecs": "0.1.7",
35
+ "@forgeax/engine-font": "0.1.7",
36
+ "@forgeax/engine-geometry": "0.1.7",
37
+ "@forgeax/engine-graphics-extras": "0.1.7",
38
+ "@forgeax/engine-image": "0.1.7",
39
+ "@forgeax/engine-input": "0.1.7",
40
+ "@forgeax/engine-math": "0.1.7",
41
+ "@forgeax/engine-pack": "0.1.7",
42
+ "@forgeax/engine-profiler": "0.1.7",
43
+ "@forgeax/engine-render": "0.1.7",
44
+ "@forgeax/engine-render-graph": "0.1.7",
45
+ "@forgeax/engine-rhi": "0.1.7",
46
+ "@forgeax/engine-rhi-null": "0.1.7",
47
+ "@forgeax/engine-rhi-webgpu": "0.1.7",
48
+ "@forgeax/engine-rhi-wgpu": "0.1.7",
49
+ "@forgeax/engine-scene": "0.1.7",
50
+ "@forgeax/engine-shader": "0.1.7",
51
+ "@forgeax/engine-skinning": "0.1.7",
52
+ "@forgeax/engine-types": "0.1.7",
53
+ "@forgeax/engine-vfx": "0.1.7"
54
54
  },
55
55
  "devDependencies": {
56
- "@forgeax/engine-import": "0.1.6",
57
- "@forgeax/engine-naga": "0.1.6",
58
- "@forgeax/engine-state": "0.1.6",
59
- "@forgeax/engine-vite-plugin-shader": "0.1.6",
56
+ "@forgeax/engine-import": "0.1.7",
57
+ "@forgeax/engine-naga": "0.1.7",
58
+ "@forgeax/engine-state": "0.1.7",
59
+ "@forgeax/engine-vite-plugin-shader": "0.1.7",
60
60
  "@types/node": "^20.14.0"
61
61
  },
62
62
  "forgeax": {
@@ -18,6 +18,7 @@ import * as SceneOwner from '@forgeax/engine-scene';
18
18
  import {
19
19
  type Asset,
20
20
  AssetRegistry,
21
+ HANDLE_CUBE,
21
22
  resolveAssetHandle,
22
23
  SceneCollectAssetGuidUnresolvedError,
23
24
  } from '@forgeax/engine-assets-runtime';
@@ -205,6 +206,38 @@ describe('m1-t2 — uncatalogued scene MISS path', () => {
205
206
  });
206
207
  });
207
208
 
209
+ describe('builtin reverse lookup ownership', () => {
210
+ it('collects a builtin mesh GUID without cataloguing the process-static payload', () => {
211
+ const registry = { guidOf: () => undefined };
212
+ const world = new World();
213
+ prepareWorld(world);
214
+ const scene: SceneAsset = {
215
+ kind: 'scene',
216
+ entities: [
217
+ {
218
+ localId: 0 as never,
219
+ components: {
220
+ Transform: {},
221
+ MeshFilter: { assetHandle: HANDLE_CUBE as never },
222
+ },
223
+ },
224
+ ],
225
+ };
226
+ const sceneHandle = world.allocSharedRef<'SceneAsset', SceneAsset>('SceneAsset', scene);
227
+ const instantiated = SceneOwner.worldInstantiateScene(world, sceneHandle);
228
+ expect(instantiated.ok).toBe(true);
229
+ if (!instantiated.ok) return;
230
+
231
+ const collected = rootsToSceneAsset(registry, world, [instantiated.value.root]);
232
+ expect(collected.ok).toBe(true);
233
+ if (!collected.ok) return;
234
+ const meshEntity = collected.value.entities.find(
235
+ (entity) => entity.components.MeshFilter !== undefined,
236
+ );
237
+ expect(meshEntity?.components.MeshFilter?.assetHandle).toBe(GUID_A);
238
+ });
239
+ });
240
+
208
241
  // ── w14 (M4) PROBE: mounted-scene-with-material identity anchor ──────────
209
242
  //
210
243
  // feat-20260713 M4 / w14 (D-6, probe-first). Empirically anchors the true
@@ -3,9 +3,22 @@ import { fileURLToPath } from 'node:url';
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
5
  type CookedRecord = {
6
- readonly key: string;
7
- readonly bytes: string;
8
- readonly values: unknown;
6
+ readonly specializationKey: string;
7
+ readonly artifactDigest: string;
8
+ readonly artifact: { readonly digest: string; readonly bytes: readonly number[] };
9
+ readonly resolved?: {
10
+ readonly values?: unknown;
11
+ };
12
+ readonly receipt?: {
13
+ readonly identity?: {
14
+ readonly layoutIdentity?: string;
15
+ readonly programIdentity?: string;
16
+ readonly pipelineIdentity?: string;
17
+ readonly cookIdentity?: string;
18
+ readonly compilerFingerprint?: string;
19
+ };
20
+ readonly schemaVersion?: string;
21
+ };
9
22
  };
10
23
 
11
24
  type MaterialRow = {
@@ -34,9 +47,29 @@ describe('custom-shader cooked MaterialAsset fixture', () => {
34
47
  const derived = fixture.assets.find((asset) => asset.payload.role === 'derived');
35
48
  expect(root?.payload.cooked).toBeDefined();
36
49
  expect(derived?.payload.cooked).toBeDefined();
37
- expect(derived?.payload.cooked?.key).toBe(root?.payload.cooked?.key);
38
- expect(derived?.payload.cooked?.bytes).toBe(root?.payload.cooked?.bytes);
39
- expect(derived?.payload.cooked?.values).toEqual(root?.payload.cooked?.values);
50
+ expect(derived?.payload.cooked?.specializationKey).toBe(
51
+ root?.payload.cooked?.specializationKey,
52
+ );
53
+ expect(derived?.payload.cooked?.artifactDigest).toBe(root?.payload.cooked?.artifactDigest);
54
+ expect(derived?.payload.cooked?.artifact.bytes).toEqual(root?.payload.cooked?.artifact.bytes);
55
+ expect(derived?.payload.cooked?.resolved?.values).toEqual(
56
+ root?.payload.cooked?.resolved?.values,
57
+ );
58
+ });
59
+
60
+ it('keeps cooked layout identity and authored coordinate transforms', () => {
61
+ const root = fixture.assets.find((asset) => asset.payload.role === 'root');
62
+ const cooked = root?.payload.cooked;
63
+ expect(cooked?.receipt?.schemaVersion).toBe('material-cook/3');
64
+ expect(cooked?.receipt?.identity?.layoutIdentity).toMatch(/^sha256-/);
65
+ expect(cooked?.receipt?.identity?.programIdentity).toMatch(/^sha256:/);
66
+ expect(cooked?.receipt?.identity?.pipelineIdentity).toMatch(/^sha256:/);
67
+ expect(cooked?.receipt?.identity?.cookIdentity).toMatch(/^sha256:/);
68
+ expect(cooked?.receipt?.identity?.compilerFingerprint).toMatch(/^sha256-/);
69
+ expect(cooked?.resolved?.values).toMatchObject({
70
+ baseColorUvTransform: [0, 0, 1, 1],
71
+ normalUvTransform: [0.125, 0.25, 2, 2],
72
+ });
40
73
  });
41
74
 
42
75
  it('runs the real Dawn path for exactly 300 frames without unexpected RHI errors', () => {
@@ -500,6 +500,18 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
500
500
  return 'render feature prepared state mismatch';
501
501
  case 'render-feature-draw-recording-failed':
502
502
  return 'render feature draw recording failed';
503
+ case 'points-lines-invalid-style':
504
+ return 'points lines invalid style';
505
+ case 'points-lines-topology-mismatch':
506
+ return 'points lines topology mismatch';
507
+ case 'points-lines-style-unsupported':
508
+ return 'points lines style unsupported';
509
+ case 'points-lines-material-unsupported':
510
+ return 'points lines material unsupported';
511
+ case 'points-lines-budget-exceeded':
512
+ return 'points lines budget exceeded';
513
+ case 'points-lines-prepare-failed':
514
+ return 'points lines prepare failed';
503
515
  }
504
516
  }
505
517
  expect(exhaustive('equirect-projection-failed')).toBe('equirect projection failed');
@@ -620,6 +632,18 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
620
632
  return 'ok';
621
633
  case 'render-feature-draw-recording-failed':
622
634
  return 'ok';
635
+ case 'points-lines-invalid-style':
636
+ return 'ok';
637
+ case 'points-lines-topology-mismatch':
638
+ return 'ok';
639
+ case 'points-lines-style-unsupported':
640
+ return 'ok';
641
+ case 'points-lines-material-unsupported':
642
+ return 'ok';
643
+ case 'points-lines-budget-exceeded':
644
+ return 'ok';
645
+ case 'points-lines-prepare-failed':
646
+ return 'ok';
623
647
  }
624
648
  }
625
649
  expect(exhaustive('hdrp-deferred-caps-insufficient')).toBe('ok');
@@ -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, last view-BG entry)', () => {
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 8 is the last entry (no binding 9)', () => {
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
- expect(entries.some((e) => e.binding === 10)).toBe(false);
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 declares no uniform buffer beyond binding 7 (only binding 0 = view, 6/7 = point/cascade)', () => {
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
- // the only uniform buffers are binding 0 (View UBO, carries the spot
2278
- // matrices in its tail), binding 6 (point shadowParams) and binding 7
2279
- // (shadowCasterCascade). No standalone spot-matrix uniform buffer.
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 is contiguous 0..8', () => {
2352
- it('exposes exactly bindings 0..8 with no gaps (binding 9 folded into View UBO, w25)', () => {
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
  });
@@ -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: 9 entries with read-only-storage on bindings 1+2', () => {
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). w25 (scope-amend
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(9);
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', () => {
@@ -84,6 +84,18 @@ function exhaustiveSwitchOnRenderCode(code: RenderErrorCode): string {
84
84
  return code;
85
85
  case 'render-feature-draw-recording-failed':
86
86
  return code;
87
+ case 'points-lines-invalid-style':
88
+ return code;
89
+ case 'points-lines-topology-mismatch':
90
+ return code;
91
+ case 'points-lines-style-unsupported':
92
+ return code;
93
+ case 'points-lines-material-unsupported':
94
+ return code;
95
+ case 'points-lines-budget-exceeded':
96
+ return code;
97
+ case 'points-lines-prepare-failed':
98
+ return code;
87
99
  default: {
88
100
  const exhaustive: never = code;
89
101
  return exhaustive;
@@ -240,6 +252,30 @@ function narrowRenderError(err: RenderError): void {
240
252
  void err.detail.authoredValue;
241
253
  void err.detail.projected;
242
254
  break;
255
+ case 'points-lines-invalid-style':
256
+ void err.detail.component;
257
+ void err.detail.field;
258
+ break;
259
+ case 'points-lines-topology-mismatch':
260
+ void err.detail.submesh;
261
+ void err.detail.actual;
262
+ break;
263
+ case 'points-lines-style-unsupported':
264
+ void err.detail.member;
265
+ void err.detail.supported;
266
+ break;
267
+ case 'points-lines-material-unsupported':
268
+ void err.detail.material;
269
+ void err.detail.pass;
270
+ break;
271
+ case 'points-lines-budget-exceeded':
272
+ void err.detail.requested;
273
+ void err.detail.limit;
274
+ break;
275
+ case 'points-lines-prepare-failed':
276
+ void err.detail.owner;
277
+ void err.detail.generation;
278
+ break;
243
279
  default: {
244
280
  const exhaustive: never = err;
245
281
  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
  });
@@ -49,6 +49,8 @@ describe('subscribe projects the discriminated Renderer event contract', () => {
49
49
  return `${event.previous}->${event.current}`;
50
50
  case 'error':
51
51
  return event.error.code;
52
+ case 'frame-submitted':
53
+ return `frame:${event.frameId}@${event.deviceGeneration}`;
52
54
  }
53
55
  };
54
56
  expectTypeOf(describeEvent).returns.toEqualTypeOf<string>();
@@ -17,6 +17,12 @@ function frameRequest(lease: Parameters<Renderer['draw']>[0]['leases'][number])
17
17
  describe('RhiNull command flow', () => {
18
18
  it('returns the synchronous receipt after the host submits a frame', async () => {
19
19
  const renderer = await requireRenderer(canvas(), { rhi }, { shaderManifestUrl: manifest });
20
+ const events: Array<{
21
+ readonly kind: string;
22
+ readonly frameId?: number;
23
+ readonly deviceGeneration?: number;
24
+ }> = [];
25
+ const unsubscribe = renderer.subscribe((event) => events.push(event));
20
26
  const world = new World();
21
27
  const attached = renderer.attach(world);
22
28
  expect(attached.ok).toBe(true);
@@ -26,9 +32,17 @@ describe('RhiNull command flow', () => {
26
32
  const frame = renderer.draw(frameRequest(attached.value));
27
33
  expect(frame.ok).toBe(true);
28
34
  if (!frame.ok) return;
35
+ expect(events.filter((event) => event.kind === 'frame-submitted')).toEqual([
36
+ {
37
+ kind: 'frame-submitted',
38
+ frameId: frame.value.frameId,
39
+ deviceGeneration: frame.value.deviceGeneration,
40
+ },
41
+ ]);
29
42
  const completed = await frame.value.completed;
30
43
  expect(completed.ok).toBe(true);
31
44
  expect(renderer.inspect().frame.frameId).toBe(frame.value.frameId);
45
+ unsubscribe();
32
46
  renderer.dispose();
33
47
  });
34
48
 
@@ -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 9-entry pbr-view BGL under storage-buffer caps', () => {
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(9); // feat-20260625 spot shadow added binding 8 view BGL grew 8→9
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 9-entry pbr-view BGL under uniform fallback caps (AC-10)', () => {
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, {
@@ -778,6 +778,10 @@ describe('post-process bindgroup identity cache (issue #670)', () => {
778
778
  views.bloomBlurV,
779
779
  );
780
780
  expect(bindGroupCreates[5]?.entries[0]?.resource.value).toBe(views.cubemap);
781
+ expect(bindGroupCreates[5]?.entries[2]?.resource.value).toEqual({
782
+ buffer: ctx.pipelineState.viewUniformBuffer,
783
+ size: 960,
784
+ });
781
785
 
782
786
  recordAll();
783
787
  expect(bindGroupCreates).toHaveLength(6);
@@ -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: AssetRegistry,
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._guidForAsset(assetRes.value as Asset);
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: AssetRegistry,
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: AssetRegistry,
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: AssetRegistry,
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._guidForAsset(pr.value as Asset);
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 { rootsToSceneAsset, serializeSceneAssetToPack } from './collect-scene-asset';
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