@forgeax/engine-render 0.1.24 → 0.1.26

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 (167) hide show
  1. package/README.md +118 -1
  2. package/dist/assembly/factory.d.ts +1 -1
  3. package/dist/assembly/factory.d.ts.map +1 -1
  4. package/dist/assembly/material/assembly.d.ts +4 -17
  5. package/dist/assembly/material/assembly.d.ts.map +1 -1
  6. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -1
  7. package/dist/assembly/material/surface-projection.d.ts +24 -0
  8. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  9. package/dist/assembly/material-shader-policy.d.ts +20 -0
  10. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  11. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  12. package/dist/assembly/webgpu-renderer.d.ts +1 -2
  13. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  14. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  15. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  16. package/dist/assets/asset-decoders.d.ts.map +1 -1
  17. package/dist/authoring.mjs +1 -1
  18. package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
  19. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  20. package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
  21. package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
  22. package/dist/{chunk-YKB5DQV7.mjs → chunk-GF523LHF.mjs} +89 -21
  23. package/dist/chunk-GF523LHF.mjs.map +1 -0
  24. package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
  25. package/dist/chunk-HKXTW355.mjs.map +1 -0
  26. package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
  27. package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
  28. package/dist/{chunk-YRUYGA4A.mjs → chunk-TMIASV2N.mjs} +944 -577
  29. package/dist/chunk-TMIASV2N.mjs.map +1 -0
  30. package/dist/{chunk-Z3YYH2MB.mjs → chunk-YUQXSEHG.mjs} +160 -73
  31. package/dist/chunk-YUQXSEHG.mjs.map +1 -0
  32. package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  33. package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
  34. package/dist/components/instances.d.ts +13 -31
  35. package/dist/components/instances.d.ts.map +1 -1
  36. package/dist/construct-renderer.mjs +1012 -149
  37. package/dist/construct-renderer.mjs.map +1 -1
  38. package/dist/extract/contracts.d.ts +6 -6
  39. package/dist/extract/contracts.d.ts.map +1 -1
  40. package/dist/gpu-driven/production-raster.d.ts +6 -1
  41. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  42. package/dist/gpu-scene.d.ts +4 -0
  43. package/dist/gpu-scene.d.ts.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.mjs +27 -9
  47. package/dist/index.mjs.map +1 -1
  48. package/dist/instance-buffer-cache.d.ts +7 -1
  49. package/dist/instance-buffer-cache.d.ts.map +1 -1
  50. package/dist/instances.d.ts +101 -0
  51. package/dist/instances.d.ts.map +1 -0
  52. package/dist/internal.mjs +5 -5
  53. package/dist/materials.d.ts +63 -2
  54. package/dist/materials.d.ts.map +1 -1
  55. package/dist/pipeline-builder.d.ts.map +1 -1
  56. package/dist/pipeline-spec-types.d.ts +2 -0
  57. package/dist/pipeline-spec-types.d.ts.map +1 -1
  58. package/dist/pipeline-spec.d.ts.map +1 -1
  59. package/dist/record/frame-snapshot.d.ts +13 -2
  60. package/dist/record/frame-snapshot.d.ts.map +1 -1
  61. package/dist/record/frame.d.ts.map +1 -1
  62. package/dist/record/main-pass-geometry.d.ts +23 -4
  63. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  64. package/dist/record/main-pass-material.d.ts +20 -2
  65. package/dist/record/main-pass-material.d.ts.map +1 -1
  66. package/dist/record/main-pass-sprite-draws.d.ts +3 -3
  67. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  68. package/dist/record/main-pass.d.ts.map +1 -1
  69. package/dist/record/material-uniforms.d.ts +4 -0
  70. package/dist/record/material-uniforms.d.ts.map +1 -0
  71. package/dist/record/render-context.d.ts +4 -3
  72. package/dist/record/render-context.d.ts.map +1 -1
  73. package/dist/record/shadow-pass.d.ts +10 -1
  74. package/dist/record/shadow-pass.d.ts.map +1 -1
  75. package/dist/recovery/render-system-candidate.d.ts +1 -1
  76. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  77. package/dist/render-contract.d.ts +4 -0
  78. package/dist/render-contract.d.ts.map +1 -1
  79. package/dist/render-system-extract-tail.d.ts +6 -0
  80. package/dist/render-system-extract-tail.d.ts.map +1 -1
  81. package/dist/render-system-extract.d.ts +44 -9
  82. package/dist/render-system-extract.d.ts.map +1 -1
  83. package/dist/render-system.d.ts +4 -1
  84. package/dist/render-system.d.ts.map +1 -1
  85. package/dist/scene/render-scene-types.d.ts +2 -0
  86. package/dist/scene/render-scene-types.d.ts.map +1 -1
  87. package/dist/scene/render-scene.d.ts +12 -1
  88. package/dist/scene/render-scene.d.ts.map +1 -1
  89. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  90. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  91. package/dist/temporal/index.mjs +5 -5
  92. package/package.json +20 -20
  93. package/src/__tests__/fixtures/ai-weapon-spirit/README.md +20 -0
  94. package/src/__tests__/fixtures/ai-weapon-spirit/low-poly-toon.wgsl +178 -0
  95. package/src/__tests__/fixtures/ai-weapon-spirit/material.json +97 -0
  96. package/src/__tests__/fixtures/ai-weapon-spirit/painterly-surface.wgsl +55 -0
  97. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +3 -3
  98. package/src/__tests__/gpu-driven-view.browser.test.ts +11 -3
  99. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  100. package/src/__tests__/instances-store.unit.test.ts +46 -0
  101. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  102. package/src/__tests__/material-contract-inventory.unit.test.ts +48 -13
  103. package/src/__tests__/{material-static-dynamic-route.unit.test.ts → material-layer-projection.unit.test.ts} +30 -47
  104. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  105. package/src/__tests__/material-program-dispatch.unit.test.ts +48 -0
  106. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  107. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  108. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  109. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
  110. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +44 -2
  111. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  112. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  113. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  114. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  115. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  116. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  117. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  118. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  119. package/src/assembly/factory.ts +2 -3
  120. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  121. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  122. package/src/assembly/material/assembly.ts +8 -40
  123. package/src/assembly/material/standard-layer-projection.ts +3 -1
  124. package/src/assembly/material/surface-projection.ts +91 -0
  125. package/src/assembly/material-shader-policy.ts +88 -10
  126. package/src/assembly/recovery/renderer-recover.ts +1 -1
  127. package/src/assembly/webgpu-ready.ts +3 -16
  128. package/src/assembly/webgpu-renderer.ts +106 -166
  129. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  130. package/src/assets/asset-decoders.ts +27 -4
  131. package/src/components/instances.ts +14 -197
  132. package/src/extract/contracts.ts +6 -6
  133. package/src/gpu-driven/production-raster.ts +34 -1
  134. package/src/gpu-scene.ts +84 -29
  135. package/src/index.ts +1 -0
  136. package/src/instance-buffer-cache.ts +14 -1
  137. package/src/instances.ts +288 -0
  138. package/src/materials.ts +109 -14
  139. package/src/pipeline-builder.ts +14 -10
  140. package/src/pipeline-spec-types.ts +2 -0
  141. package/src/pipeline-spec.ts +13 -17
  142. package/src/record/frame-snapshot.ts +19 -2
  143. package/src/record/frame.ts +38 -3
  144. package/src/record/main-pass-geometry.ts +837 -337
  145. package/src/record/main-pass-material.ts +87 -2
  146. package/src/record/main-pass-sprite-draws.ts +66 -169
  147. package/src/record/main-pass.ts +24 -246
  148. package/src/record/material-uniforms.ts +186 -0
  149. package/src/record/render-context.ts +8 -0
  150. package/src/record/shadow-pass.ts +120 -115
  151. package/src/recovery/render-system-candidate.ts +46 -5
  152. package/src/render-contract.ts +4 -0
  153. package/src/render-system-extract-tail.ts +153 -57
  154. package/src/render-system-extract.ts +110 -73
  155. package/src/render-system.ts +59 -5
  156. package/src/scene/render-scene-types.ts +2 -0
  157. package/src/scene/render-scene.ts +579 -36
  158. package/src/systems/skin-palette-allocator.ts +129 -7
  159. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  160. package/dist/assembly/material/pipeline-projection.d.ts +0 -30
  161. package/dist/assembly/material/pipeline-projection.d.ts.map +0 -1
  162. package/dist/chunk-KZFTK6DO.mjs.map +0 -1
  163. package/dist/chunk-Q752SHI5.mjs.map +0 -1
  164. package/dist/chunk-YKB5DQV7.mjs.map +0 -1
  165. package/dist/chunk-YRUYGA4A.mjs.map +0 -1
  166. package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
  167. package/src/assembly/material/pipeline-projection.ts +0 -63
@@ -15,6 +15,7 @@ import {
15
15
  import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
16
16
  import { box3, frustum, type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
17
17
  import { AssetGuid } from '@forgeax/engine-pack/guid';
18
+ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-cook';
18
19
  import { RhiError } from '@forgeax/engine-rhi';
19
20
  import { GlobalTransform, MorphWeights, Transform } from '@forgeax/engine-scene';
20
21
  import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
@@ -88,7 +89,8 @@ import type {
88
89
  } from './features/types';
89
90
  import { ensureGlyphMeshMaterialSlots } from './glyph-text-layout-system';
90
91
  import type { GpuDrivenGeometryReceipt } from './gpu-driven/prepared-draw';
91
- import { deriveInstancesUnionBounds } from './instances-derived-bounds';
92
+ import type { InstanceProjectionStore } from './instances';
93
+ import { deriveInstancesUnionBounds, fingerprintNumericArray } from './instances-derived-bounds';
92
94
  import {
93
95
  type MeshMaterialBindingDiagnostic,
94
96
  type MeshMaterialBindingSource,
@@ -147,6 +149,7 @@ import {
147
149
  materialColorParameterSchema,
148
150
  materialNormalScale,
149
151
  materialParametersToParamSchema,
152
+ materialProgramKeysForMaterial,
150
153
  materialTextureFields,
151
154
  materialTextureRef,
152
155
  pipelineRenderState,
@@ -156,7 +159,7 @@ import {
156
159
  readStablePersistentMaterialSnapshot,
157
160
  resolveMaterialSnapshot,
158
161
  resolveVideoFieldHandle,
159
- runtimeMaterialShaderId,
162
+ runtimeMaterialShaderIdForMaterial,
160
163
  Severity,
161
164
  selectCameraRoles,
162
165
  selectFogFrame,
@@ -173,11 +176,15 @@ export function extractFrames(
173
176
  pipelineState?: ExtractPipelineSurface | null,
174
177
  materialSnapshotCachesByWorld?: MaterialSnapshotCachesByWorld,
175
178
  options: {
179
+ readonly materialContext?: MaterialCookProgramContext;
176
180
  readonly cull?: 'normal' | 'none';
177
181
  readonly renderables?: 'full' | 'none';
182
+ readonly renderableEntitiesByWorld?: readonly (ReadonlySet<number> | undefined)[];
183
+ readonly retainHidden?: boolean;
178
184
  readonly getMaterialShaderArtifact?: (
179
185
  materialShaderId: string,
180
186
  ) => MaterialShaderArtifact | undefined;
187
+ readonly instanceCollections?: InstanceProjectionStore;
181
188
  } = {},
182
189
  ): ExtractedFrame {
183
190
  // w4: normalize the owner argument. A bare number is the legacy single-owner
@@ -247,11 +254,21 @@ export function extractFrames(
247
254
  return cache;
248
255
  })(),
249
256
  }),
257
+ ...(options.materialContext === undefined
258
+ ? {}
259
+ : { materialContext: options.materialContext }),
250
260
  cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
251
261
  renderables: options.renderables ?? 'full',
262
+ ...(options.renderableEntitiesByWorld?.[wi] === undefined
263
+ ? {}
264
+ : { renderableEntities: options.renderableEntitiesByWorld[wi] }),
265
+ retainHidden: options.retainHidden ?? false,
252
266
  ...(options.getMaterialShaderArtifact === undefined
253
267
  ? {}
254
268
  : { getMaterialShaderArtifact: options.getMaterialShaderArtifact }),
269
+ ...(options.instanceCollections === undefined
270
+ ? {}
271
+ : { instanceCollections: options.instanceCollections }),
255
272
  ...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
256
273
  });
257
274
  const frame = extractFrame(world, prepared);
@@ -495,6 +512,30 @@ export function extractFrames(
495
512
  return (f.reflectionProbes ?? []).map((probe) => ({ ...probe, worldId }));
496
513
  });
497
514
 
515
+ if (options.renderables !== 'none' && options.renderableEntitiesByWorld === undefined) {
516
+ options.instanceCollections?.retain(
517
+ new Set(
518
+ renderables.flatMap((row) =>
519
+ row.instances?.collectionId === undefined ? [] : [row.instances.collectionId],
520
+ ),
521
+ ),
522
+ );
523
+ if (
524
+ skinPaletteAllocator !== null &&
525
+ typeof skinPaletteAllocator.retainSliceKeys === 'function'
526
+ ) {
527
+ skinPaletteAllocator.retainSliceKeys(
528
+ new Set(
529
+ renderables.flatMap((row) =>
530
+ row.skin === undefined
531
+ ? []
532
+ : [`${worlds[row.worldId]?.identity ?? row.worldId}:${row.entityKey}`],
533
+ ),
534
+ ),
535
+ );
536
+ }
537
+ }
538
+
498
539
  return {
499
540
  cameras,
500
541
  auxiliaryCameras,
@@ -604,6 +645,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
604
645
  cull: cullMode,
605
646
  resourceOwnerFog,
606
647
  getMaterialShaderArtifact,
648
+ instanceCollections,
649
+ renderableEntities,
650
+ retainHidden,
607
651
  } = context;
608
652
  // Environment reads are owned by the retained resource projection and are
609
653
  // established at the frame boundary; record never revisits World state.
@@ -1521,22 +1565,18 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1521
1565
  // extraction. Shared handles are immutable inputs for the frame, while
1522
1566
  // dispatch entries still need to be rebuilt for each entity.
1523
1567
  const materialSnapshotCache: MaterialSnapshotCache = new Map();
1568
+ const skinnedMaterialSnapshotCache: MaterialSnapshotCache = new Map();
1524
1569
  // tweak-20260611 M1: MeshRenderer renderable archetype walk routes
1525
1570
  // through one World-owned Query. K-2 sniffing scheme B
1526
1571
  // (`row.get(X) !== undefined` edge sniff) replaces the prior
1527
1572
  // `arch.components.some` row-internal back-door. K-3 invariant: the
1528
- // variable-length array reads (`MeshRenderer.materials`,
1529
- // `Instances.transforms`) still flow through `_getArrayView` /
1530
- // `world.get(e, Instances)` -- only the `entity` source switches to
1531
- // `row.entity`.
1573
+ // variable-length array reads (`MeshRenderer.materials`, SpriteInstances
1574
+ // arrays) still flow through `_getArrayView`; explicit Instances bytes also come
1575
+ // from World, before projection into renderer-owned snapshots.
1532
1576
  //
1533
- // archVersion plumbing exception: the `RenderableSnapshot.instances
1534
- // .archVersion` cache key is keyed off the live archetype's mutation
1535
- // counter (record stage `instanceBuffers` cache invalidation). The
1536
- // QueryRow does not surface this number, so a single archetype
1537
- // graph access is retained for instance-bearing rows -- match by entity
1538
- // packed handle. AC-01 grep ≤ current - 1 still holds: the stale
1539
- // archetype-graph traversal commentary at the prior call site is gone.
1577
+ // archVersion remains the content fingerprint for SpriteInstances' managed
1578
+ // arrays; explicit Instances use the collection revision below and never
1579
+ // need an archetype graph access for their matrix bytes.
1540
1580
  // feat-20260521-sprite-atlas-animation M3 / T-16: SpriteRegionOverride
1541
1581
  // column id for the sprite-bucket per-entity region override read
1542
1582
  void SpriteRegionOverride;
@@ -1564,11 +1604,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1564
1604
  })
1565
1605
  .unwrap();
1566
1606
  // QueryRow deliberately exposes component data, not an archetype version.
1567
- // The optimized ECS owner keeps table storage private, so instance-buffer
1568
- // invalidation uses a content fingerprint of the managed arrays instead.
1607
+ // SpriteInstances still uses a content fingerprint for its managed arrays;
1608
+ // explicit Instances use the renderer-owned collection revision below.
1569
1609
  const resolveArchVersion = (entity: EntityHandle): number => {
1570
1610
  const values = [
1571
- worldInternal._getArrayView(entity, Instances, 'transforms'),
1572
1611
  worldInternal._getArrayView(entity, SpriteInstances, 'transforms'),
1573
1612
  worldInternal._getArrayView(entity, SpriteInstances, 'regions'),
1574
1613
  ];
@@ -1591,6 +1630,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1591
1630
  for (const entry of pending) dispatch.push(entry);
1592
1631
  };
1593
1632
  for (const row of meshRendererQuery) {
1633
+ if (
1634
+ context.renderables === 'full' &&
1635
+ renderableEntities !== undefined &&
1636
+ !renderableEntities.has(row.entity)
1637
+ ) {
1638
+ continue;
1639
+ }
1594
1640
  // K-2 archetype-edge sniff (scheme B): a missing optional component
1595
1641
  // surfaces as an absent bundle key, not a row-internal optional chain.
1596
1642
  // Presence-only checks use QueryRow.has so large array-bearing components
@@ -1687,8 +1733,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1687
1733
  {
1688
1734
  // feat-20260608 M2 / w11: read materials array via _getArrayView
1689
1735
  const entity = row.entity;
1690
- if (isRenderable && visibility?.effective(entity) === 'hidden') {
1691
- explicitlyHidden.add(entity);
1736
+ const authorVisible = !(isRenderable && visibility?.effective(entity) === 'hidden');
1737
+ if (!authorVisible) explicitlyHidden.add(entity);
1738
+ if (!authorVisible && !retainHidden) {
1692
1739
  continue;
1693
1740
  }
1694
1741
  const layerVal = fLayerValue ?? 0;
@@ -1886,6 +1933,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1886
1933
  renderables.length,
1887
1934
  layerVal,
1888
1935
  materialSnap.paramSnapshot,
1936
+ 0,
1937
+ materialSnap.materialProgramKeys,
1889
1938
  );
1890
1939
  }
1891
1940
  } else if (handleRaw === 0 || assets === undefined || assets === null) {
@@ -1913,6 +1962,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1913
1962
  renderables.length,
1914
1963
  layerVal,
1915
1964
  materialSnap.paramSnapshot,
1965
+ 0,
1966
+ materialSnap.materialProgramKeys,
1916
1967
  );
1917
1968
  }
1918
1969
  } else {
@@ -1976,10 +2027,26 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1976
2027
  }
1977
2028
  const resolved = resolvedResult.value;
1978
2029
  const allPasses = resolved.passes;
1979
- const firstPassShader =
1980
- allPasses.length > 0
1981
- ? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
1982
- : undefined;
2030
+ const materialContext =
2031
+ context.materialContext === undefined
2032
+ ? undefined
2033
+ : {
2034
+ ...context.materialContext,
2035
+ geometry: hasSkin ? ('skinned' as const) : context.materialContext.geometry,
2036
+ };
2037
+ const materialProgramKeys = materialProgramKeysForMaterial(
2038
+ asset,
2039
+ assets,
2040
+ materialContext,
2041
+ );
2042
+ const authoredFirstPassShader = runtimeMaterialShaderIdForMaterial(
2043
+ allPasses,
2044
+ undefined,
2045
+ );
2046
+ const firstPassShader = runtimeMaterialShaderIdForMaterial(
2047
+ allPasses,
2048
+ materialProgramKeys,
2049
+ );
1983
2050
  const pv = materialValuesToLinearRuntime(
1984
2051
  resolved.values,
1985
2052
  materialColorParameterSchema(resolved.parameters ?? [], firstPassShader, assets),
@@ -2008,7 +2075,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2008
2075
 
2009
2076
  const materialParamSchema = materialParametersToParamSchema(
2010
2077
  resolved.parameters ?? [],
2011
- firstPassShader,
2078
+ authoredFirstPassShader,
2012
2079
  );
2013
2080
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
2014
2081
  // bidirectional Skin <-> pbr-skin material fail-fast at extract.
@@ -2297,6 +2364,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2297
2364
  }),
2298
2365
  normalScale: normalScalePv,
2299
2366
  materialShaderId: firstPassShader,
2367
+ materialProgramKeys,
2300
2368
  materialHandle: handleRaw,
2301
2369
  renderState: pipelineRenderState(allPasses[0]?.renderState),
2302
2370
  paramSnapshot: paramSnap,
@@ -2357,6 +2425,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2357
2425
  renderables.length,
2358
2426
  layerVal,
2359
2427
  paramSnap,
2428
+ 0,
2429
+ materialProgramKeys,
2360
2430
  );
2361
2431
  }
2362
2432
  }
@@ -2512,7 +2582,18 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2512
2582
  // surface is absent (test fixtures that pass undefined) the
2513
2583
  // hasSkin segment is skipped silently — bind-pose equivalent.
2514
2584
  if (skinPaletteAllocator !== null) {
2515
- const slice = skinPaletteAllocator.allocateSlice(skeleton.jointCount);
2585
+ // Keep the narrow extraction seam compatible with older test or
2586
+ // host-owned allocator doubles while the real renderer uses the
2587
+ // stable-key path. The fallback is frame-local and therefore
2588
+ // cannot affect production allocators that implement the new
2589
+ // method.
2590
+ const slice =
2591
+ typeof skinPaletteAllocator.allocateSliceFor === 'function'
2592
+ ? skinPaletteAllocator.allocateSliceFor(
2593
+ `${world.identity}:${entity}`,
2594
+ skeleton.jointCount,
2595
+ )
2596
+ : skinPaletteAllocator.allocateSlice(skeleton.jointCount);
2516
2597
  skinPaletteAllocator.writeJointPalette(slice, ibms, jointWorlds);
2517
2598
  skinSlice = {
2518
2599
  jointCount: slice.jointCount,
@@ -2556,15 +2637,22 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2556
2637
  if (assets !== undefined && assets !== null) {
2557
2638
  for (let mi = 1; mi < materialHandles.length; mi++) {
2558
2639
  const subHandle = materialHandles[mi] ?? 0;
2559
- const cachedSubmaterial = materialSnapshotCache.get(subHandle);
2640
+ const subCache = hasSkin ? skinnedMaterialSnapshotCache : materialSnapshotCache;
2641
+ const cachedSubmaterial = subCache.get(subHandle);
2560
2642
  materialsArr.push(
2561
2643
  cachedSubmaterial?.snapshot ??
2562
2644
  resolveMaterialSnapshot(
2563
2645
  subHandle,
2564
2646
  world,
2565
2647
  assets,
2566
- materialSnapshotCache,
2567
- persistentMaterialSnapshotCache,
2648
+ subCache,
2649
+ hasSkin ? undefined : persistentMaterialSnapshotCache,
2650
+ context.materialContext === undefined
2651
+ ? undefined
2652
+ : {
2653
+ ...context.materialContext,
2654
+ geometry: hasSkin ? 'skinned' : context.materialContext.geometry,
2655
+ },
2568
2656
  ),
2569
2657
  );
2570
2658
  }
@@ -2574,8 +2662,14 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2574
2662
  const subHandle = materialHandles[mi] ?? 0;
2575
2663
  if (subHandle === handleRaw) continue;
2576
2664
  const subEntry =
2577
- materialSnapshotCache.get(subHandle) ??
2578
- readPersistentMaterialSnapshot(persistentMaterialSnapshotCache, subHandle, assets);
2665
+ (hasSkin ? skinnedMaterialSnapshotCache : materialSnapshotCache).get(subHandle) ??
2666
+ (hasSkin
2667
+ ? undefined
2668
+ : readPersistentMaterialSnapshot(
2669
+ persistentMaterialSnapshotCache,
2670
+ subHandle,
2671
+ assets,
2672
+ ));
2579
2673
  appendMaterialDispatchEntries(
2580
2674
  pendingDispatch,
2581
2675
  subEntry?.passes ?? (subHandle === 0 ? [DEFAULT_FORWARD_PASS] : []),
@@ -2584,6 +2678,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2584
2678
  renderables.length,
2585
2679
  layerVal,
2586
2680
  materialsArr[mi]?.paramSnapshot,
2681
+ 0,
2682
+ materialsArr[mi]?.materialProgramKeys,
2587
2683
  );
2588
2684
  }
2589
2685
  }
@@ -2727,39 +2823,35 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2727
2823
  // Instances are snapshotted before the CPU visibility decision so the
2728
2824
  // culling owner can derive the union of every instance's transformed
2729
2825
  // mesh bounds. The same detached snapshot is then carried to record;
2730
- // do not re-read the ECS array after culling (a structural write must
2731
- // not make the cull and draw disagree within one frame).
2826
+ // do not re-read the collection after culling (a mutation must not
2827
+ // make the cull and draw disagree within one frame).
2732
2828
  let instancesSnap: InstancesSnapshot | undefined;
2733
2829
  if (hasInstances) {
2734
- const transforms = worldInternal._getArrayView(entity, Instances, 'transforms');
2735
- if (transforms === undefined) {
2736
- // A present component with an unavailable array is an empty
2737
- // instance contribution, not an absent component. Preserve the
2738
- // zero-draw contract so the record stage cannot synthesize one
2739
- // identity instance from an incomplete row.
2740
- instancesSnap = {
2741
- transforms: new Float32Array(),
2742
- instanceCount: 0,
2743
- cacheKey: entity,
2744
- archVersion,
2745
- };
2746
- } else {
2747
- const actualLength = transforms.length;
2748
- if (actualLength % 16 !== 0) {
2749
- worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
2830
+ const transforms =
2831
+ worldInternal._getArrayView(entity, Instances, 'transforms') ?? new Float32Array();
2832
+ if (transforms.length % 16 !== 0) {
2833
+ worldInternal._routeError(
2834
+ new InstanceTransformsStrideMismatchError(transforms.length),
2835
+ {
2750
2836
  severity: Severity.Error,
2751
2837
  systemName: 'RenderSystem.extract (Instances stride)',
2752
- });
2753
- continue;
2754
- }
2755
- const snapshotCopy = new Float32Array(transforms);
2756
- instancesSnap = {
2757
- transforms: snapshotCopy,
2758
- instanceCount: actualLength / 16,
2759
- cacheKey: entity,
2760
- archVersion,
2761
- };
2838
+ },
2839
+ );
2840
+ continue;
2762
2841
  }
2842
+ const projected = instanceCollections?.project(world, entity, transforms);
2843
+ instancesSnap = {
2844
+ transforms: projected?.transforms ?? new Float32Array(transforms),
2845
+ instanceCount: transforms.length / 16,
2846
+ cacheKey: entity,
2847
+ archVersion: projected === undefined ? fingerprintNumericArray(transforms) : 0,
2848
+ ...(projected === undefined
2849
+ ? {}
2850
+ : {
2851
+ collectionId: projected.collectionId,
2852
+ revision: projected.revision,
2853
+ }),
2854
+ };
2763
2855
  }
2764
2856
 
2765
2857
  const baseRenderable: RenderableSnapshot = {
@@ -2774,7 +2866,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2774
2866
  materialBindingDiagnostics,
2775
2867
  worldId: 0,
2776
2868
  entityKey: entity,
2869
+ ...(authorVisible ? {} : { authorVisible: false }),
2777
2870
  ...(skinSlice !== undefined ? { skin: skinSlice } : {}),
2871
+ ...(skin?.joints === undefined
2872
+ ? {}
2873
+ : { skinJointEntities: Array.from(skin.joints, (joint) => Number(joint)) }),
2778
2874
  ...(morph !== undefined ? { morph } : {}),
2779
2875
  ...(instancesSnap !== undefined ? { instances: instancesSnap } : {}),
2780
2876
  ...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),