@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
@@ -1,3 +1,4 @@
1
+ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-cook';
1
2
  // @forgeax/engine-runtime - RenderSystem Extract stage (D-S2 + plan-strategy R-15
2
3
  // fallback split). Pure ECS query phase: walks Camera / DirectionalLight /
3
4
  // the merged-MeshRenderer renderable archetype and produces SoA-free snapshot
@@ -75,31 +76,15 @@
75
76
  // `MeshFilter.assetHandle` dangling path
76
77
  // (charter proposition 5; AC-11).
77
78
  //
78
- // feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15:
79
- // The legacy `Instances { buffer, count }` form (cross-coupled with the
80
- // deleted `AssetRegistry.createInstancedBuffer` triplet + the retired
81
- // `InstancedBufferAsset` POD) is gone. Per-entity instance transforms now
82
- // live inside the ECS `Instances { transforms: 'array<f32>' }` column;
83
- // extract materialises a `Float32Array` snapshot per Instances-bearing
84
- // entity (via `world.get(e, Instances).transforms`) so the record stage
85
- // can upload to a per-entity GPU storage buffer cached by entity packed
86
- // u32.
87
- //
88
- // feat-20260515-buffer-array-vocab-collapse M3 / w15: the component-level
89
- // `arrayStride` hook on `Instances.transforms` was retired (decision section
90
- // 2.3 stride responsibility migration -- AI users gate at the set / push
91
- // call sites + RenderSystem extract entry holds a defensive fail-fast).
92
- // The extract entry checks `transforms.length % 16 === 0` directly after
93
- // the `world.get(entity, Instances)` snapshot is taken; violations route
94
- // `InstanceTransformsStrideMismatchError`
95
- // (`code: 'instance-transforms-stride-mismatch'`,
96
- // `detail: { actualLength, expectedStride: 16 }`) through the World
97
- // Layer-3 ErrorHandler and the renderable is skipped. The record stage
98
- // trusts the invariant and does not re-check.
79
+ // World owns explicit Instances transforms. Extraction makes a detached
80
+ // per-renderer projection; GPU residents acknowledge complete snapshots only
81
+ // after successful uploads. Reading a snapshot never changes author state.
99
82
 
100
83
  import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
101
84
  import {
85
+ materialParametersToParamSchema as projectMaterialParametersToParamSchema,
102
86
  resolveAssetHandle,
87
+ selectMaterialPassProgram,
103
88
  walkMaterialPassesOverSharedRefs,
104
89
  } from '@forgeax/engine-assets-runtime';
105
90
  import type { Component, ComponentSchema, EntityHandle, Query, World } from '@forgeax/engine-ecs';
@@ -166,6 +151,7 @@ import type {
166
151
  RenderFeatureHiddenEntityReport,
167
152
  RenderFeatureWorldVisibilitySnapshot,
168
153
  } from './features/types';
154
+ import type { InstanceCollectionId, InstanceProjectionStore } from './instances';
169
155
  import type {
170
156
  MeshMaterialBindingDiagnostic,
171
157
  MeshMaterialBindingSource,
@@ -696,6 +682,8 @@ export interface RenderableSnapshot {
696
682
  readonly materialBindingSources: readonly MeshMaterialBindingSource[];
697
683
  /** Active structured override diagnostics for this renderable. */
698
684
  readonly materialBindingDiagnostics?: readonly MeshMaterialBindingDiagnostic[];
685
+ /** Effective authored visibility retained by the persistent scene owner. */
686
+ readonly authorVisible?: boolean;
699
687
  /**
700
688
  * feat-20260708-composited-multi-world-rendering M1 / D-1: the worldId
701
689
  * of the world this renderable was extracted from. Defaults to 0 in
@@ -761,6 +749,8 @@ export interface RenderableSnapshot {
761
749
  * Absent (`undefined`) means the entity is not skinned.
762
750
  */
763
751
  readonly skin?: SkinPaletteSlice;
752
+ /** Joint entity identities used by the persistent skin dependency index. */
753
+ readonly skinJointEntities?: readonly number[];
764
754
  /**
765
755
  * ECS-owned morph weights. Presence selects the Standard Pipeline CPU
766
756
  * specialized deformation lane; the asset remains the source of target
@@ -822,10 +812,14 @@ export interface GpuDrivenDrawSnapshot {
822
812
  }
823
813
 
824
814
  export interface InstancesSnapshot {
815
+ /** Renderer-owned collection identity. */
816
+ readonly collectionId?: InstanceCollectionId;
825
817
  /** Packed column-major mat4 transforms (16 f32 per instance). */
826
818
  readonly transforms: Float32Array;
827
819
  /** Number of instances (transforms.length / 16). */
828
820
  readonly instanceCount: number;
821
+ /** Collection revision captured by this detached frame snapshot. */
822
+ readonly revision?: number;
829
823
  /** Stable per-entity GPU buffer cache key (the packed Entity u32). */
830
824
  readonly cacheKey: number;
831
825
  /**
@@ -918,6 +912,7 @@ export interface MaterialSnapshot {
918
912
  * discriminator (M4-T06).
919
913
  */
920
914
  readonly materialShaderId?: string | undefined;
915
+ readonly materialProgramKeys?: Readonly<Record<string, string>> | undefined;
921
916
  /**
922
917
  * Stable source handle for the material asset. Record uses this only for
923
918
  * same-frame assembly reuse; GPU bind-group validity still comes from the
@@ -1024,12 +1019,9 @@ export interface MaterialSnapshot {
1024
1019
  * from the legacy `shadingModel` discriminant (feat-20260625 M2 D-3,
1025
1020
  * finalised in w15; `shadingModel` field removed in tweak-20260701 M1).
1026
1021
  *
1027
- * Extract derives this from the first pass's `renderState.blend !==
1028
- * undefined` (post-feat-20260626-collapse: blend presence is the
1029
- * single SSOT for "this material is transparent on the geometry
1030
- * pipeline cache key"). Multi-pass materials whose mix of opaque +
1031
- * transparent passes need finer routing should split into separate
1032
- * MaterialAsset entries (the normal forgeax pattern).
1022
+ * This describes the primary-pass snapshot for consumers that need a
1023
+ * material summary. Actual draw routing and the LDR split read the selected
1024
+ * DispatchEntry.renderState, so mixed opaque/transparent Passes remain valid.
1033
1025
  *
1034
1026
  * Type is `boolean | undefined` (derived): `undefined` means "no
1035
1027
  * passes / unknown"; consumers must read `=== true` / `!== true` to
@@ -1134,6 +1126,7 @@ export function appendMaterialDispatchEntries(
1134
1126
  layer: number,
1135
1127
  paramSnapshot: Readonly<Record<string, number | number[] | string>> | undefined,
1136
1128
  passIndexOffset = 0,
1129
+ materialProgramKeys?: Readonly<Record<string, string>>,
1137
1130
  ): void {
1138
1131
  const matchedPasses = selectPasses(passes, {});
1139
1132
  for (let pIdx = 0; pIdx < matchedPasses.length; pIdx++) {
@@ -1144,6 +1137,11 @@ export function appendMaterialDispatchEntries(
1144
1137
  readonly queue?: number;
1145
1138
  readonly stencilReference?: number;
1146
1139
  };
1140
+ const authoredShaderId = runtimeMaterialShaderId(pass.program.module, pass.name);
1141
+ const dispatchShaderId =
1142
+ materialProgramKeys === undefined ? authoredShaderId : materialProgramKeys[pass.name];
1143
+ if (materialProgramKeys !== undefined && dispatchShaderId === undefined)
1144
+ throw new Error(`Missing published material program for Pass ${pass.name}`);
1147
1145
  pendingDispatch.push({
1148
1146
  entityIndex: entity,
1149
1147
  materialHandle,
@@ -1158,7 +1156,7 @@ export function appendMaterialDispatchEntries(
1158
1156
  defines: undefined,
1159
1157
  vertexEntry: pass.program.vertexEntry,
1160
1158
  fragmentEntry: pass.program.fragmentEntry,
1161
- materialShaderId: runtimeMaterialShaderId(pass.program.module, pass.name),
1159
+ materialShaderId: dispatchShaderId,
1162
1160
  paramSnapshot,
1163
1161
  ...(passState.stencilReference !== undefined && {
1164
1162
  stencilReference: passState.stencilReference,
@@ -1410,6 +1408,57 @@ export function runtimeMaterialShaderId(
1410
1408
  }
1411
1409
  }
1412
1410
 
1411
+ /**
1412
+ * Select the runtime identity for one resolved material. A cooked publication
1413
+ * owns the composed material artifact identity; the authored pass module is
1414
+ * the built-in fallback used before that publication is ready.
1415
+ */
1416
+ export function materialProgramKeysForMaterial(
1417
+ material: MaterialAsset,
1418
+ assets: AssetRegistry,
1419
+ context: MaterialCookProgramContext | undefined,
1420
+ ): Readonly<Record<string, string>> | undefined {
1421
+ const projection = assets.getMaterialProjectionForPayload(material);
1422
+ if (projection === undefined) return undefined;
1423
+ if (context === undefined)
1424
+ throw new Error('Published material selection requires renderer-owned compiler context');
1425
+ return Object.fromEntries(
1426
+ projection.passes.map((pass) => {
1427
+ const mode = String(
1428
+ (pass.renderState?.tags as Record<string, unknown> | undefined)?.LightMode ?? pass.name,
1429
+ );
1430
+ const selectedContext: MaterialCookProgramContext = {
1431
+ ...context,
1432
+ pass: /shadow/i.test(mode) ? 'shadow' : /depth/i.test(mode) ? 'depth' : 'forward',
1433
+ pipeline: /deferred|gbuffer/i.test(mode) ? 'deferred' : context.pipeline,
1434
+ };
1435
+ return [
1436
+ pass.name,
1437
+ selectMaterialPassProgram(projection, pass.name, selectedContext).specializationKey,
1438
+ ];
1439
+ }),
1440
+ );
1441
+ }
1442
+
1443
+ export function runtimeMaterialShaderIdForMaterial(
1444
+ passes: readonly MaterialPass[],
1445
+ programs: Readonly<Record<string, string>> | undefined,
1446
+ ): string | undefined {
1447
+ const pass =
1448
+ passes.find(
1449
+ (pass) =>
1450
+ !/shadow|depth/i.test(
1451
+ String(
1452
+ (pass.renderState?.tags as Record<string, unknown> | undefined)?.LightMode ?? pass.name,
1453
+ ),
1454
+ ),
1455
+ ) ?? passes[0];
1456
+ if (pass === undefined) return undefined;
1457
+ return programs === undefined
1458
+ ? runtimeMaterialShaderId(pass.program.module, pass.name)
1459
+ : programs[pass.name];
1460
+ }
1461
+
1413
1462
  export function pipelineRenderState(
1414
1463
  renderState:
1415
1464
  | (MaterialRenderState & {
@@ -1714,49 +1763,21 @@ export function isEngineInjectedTextureField(
1714
1763
  return isStandardPbrMaterialShader(shaderId) && ENGINE_INJECTED_TEXTURE_FIELDS.has(fieldName);
1715
1764
  }
1716
1765
 
1766
+ /**
1767
+ * Project one material's authoring parameters into the runtime ABI.
1768
+ *
1769
+ * Engine-owned Standard roots are compiled against the canonical superset
1770
+ * schema even when the authoring asset omits optional physical fields. A
1771
+ * compact authored schema would move later UBO members (for example `ior`)
1772
+ * into the wrong slots. Custom material roots keep their own published
1773
+ * parameter projection.
1774
+ */
1717
1775
  export function materialParametersToParamSchema(
1718
1776
  parameters: readonly MaterialParameter[],
1719
- shaderId: string | undefined,
1777
+ shaderId?: string,
1720
1778
  ): readonly ParamSchemaEntry[] {
1721
- // Engine-owned Standard modules are compiled once against the canonical
1722
- // boot schema. Materials.standard() exposes a smaller authoring projection,
1723
- // but using that compact projection as the runtime UBO schema would shift
1724
- // every field after an omitted physical entry.
1725
- if (isCanonicalStandardPbrMaterialShader(shaderId)) {
1726
- return STANDARD_PIPELINE_PARAM_SCHEMA;
1727
- }
1728
- const engineInjectedTextureFields = isStandardPbrMaterialShader(shaderId)
1729
- ? ENGINE_INJECTED_TEXTURE_FIELDS
1730
- : undefined;
1731
- return parameters.flatMap((parameter): ParamSchemaEntry[] => {
1732
- // Boolean material values are runtime-only and have no UBO representation.
1733
- if (parameter.type === 'bool') return [];
1734
- if (parameter.type === 'texture' || parameter.type === 'texture_cube') {
1735
- if (engineInjectedTextureFields?.has(parameter.name) === true) return [];
1736
- return [
1737
- {
1738
- name: parameter.name,
1739
- type: parameter.type === 'texture_cube' ? 'texture_cube' : 'texture2d',
1740
- default: parameter.default,
1741
- },
1742
- ];
1743
- }
1744
- const numericDefault =
1745
- typeof parameter.default === 'number'
1746
- ? parameter.default
1747
- : Array.isArray(parameter.default) &&
1748
- parameter.default.every((entry) => typeof entry === 'number')
1749
- ? parameter.default
1750
- : undefined;
1751
- const numericEntry = {
1752
- name: parameter.name,
1753
- type: parameter.type,
1754
- ...(parameter.colorSpace === undefined ? {} : { colorSpace: parameter.colorSpace }),
1755
- };
1756
- return numericDefault === undefined
1757
- ? [numericEntry]
1758
- : [{ ...numericEntry, default: numericDefault }];
1759
- });
1779
+ if (isCanonicalStandardPbrMaterialShader(shaderId)) return STANDARD_PIPELINE_PARAM_SCHEMA;
1780
+ return projectMaterialParametersToParamSchema(parameters);
1760
1781
  }
1761
1782
 
1762
1783
  /**
@@ -1903,6 +1924,7 @@ export function resolveMaterialSnapshot(
1903
1924
  assetsRef: AssetRegistry,
1904
1925
  materialSnapshotCache?: MaterialSnapshotCache,
1905
1926
  persistentMaterialSnapshotCache?: MaterialSnapshotCache,
1927
+ materialContext?: MaterialCookProgramContext,
1906
1928
  ): MaterialSnapshot {
1907
1929
  if (handleRaw === 0) return defaultMaterialSnapshot(handleRaw);
1908
1930
  const cached = materialSnapshotCache?.get(handleRaw);
@@ -1934,10 +1956,9 @@ export function resolveMaterialSnapshot(
1934
1956
  if (!resolvedResult.ok) return defaultMaterialSnapshot(handleRaw);
1935
1957
  const resolved = resolvedResult.value;
1936
1958
  const allPasses = resolved.passes;
1937
- const firstPassShader =
1938
- allPasses.length > 0
1939
- ? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
1940
- : undefined;
1959
+ const materialProgramKeys = materialProgramKeysForMaterial(asset, assetsRef, materialContext);
1960
+ const authoredFirstPassShader = runtimeMaterialShaderIdForMaterial(allPasses, undefined);
1961
+ const firstPassShader = runtimeMaterialShaderIdForMaterial(allPasses, materialProgramKeys);
1941
1962
  const pv = materialValuesToLinearRuntime(
1942
1963
  resolved.values,
1943
1964
  materialColorParameterSchema(resolved.parameters ?? [], firstPassShader, assetsRef),
@@ -1964,7 +1985,7 @@ export function resolveMaterialSnapshot(
1964
1985
  }
1965
1986
  const materialParamSchema = materialParametersToParamSchema(
1966
1987
  resolved.parameters ?? [],
1967
- firstPassShader,
1988
+ authoredFirstPassShader,
1968
1989
  );
1969
1990
  // feat-20260614 M8 (D-19): texture / sampler values are embedded GUIDs
1970
1991
  // (dash-form strings) after loadByGuid. Resolve each to a user-tier column
@@ -2032,6 +2053,7 @@ export function resolveMaterialSnapshot(
2032
2053
  }),
2033
2054
  normalScale: normalScalePv,
2034
2055
  materialShaderId: firstPassShader,
2056
+ materialProgramKeys,
2035
2057
  materialHandle: handleRaw,
2036
2058
  renderState: pipelineRenderState(allPasses[0]?.renderState),
2037
2059
  paramSnapshot: paramSnap,
@@ -2533,6 +2555,7 @@ export interface ExtractFramesOwner {
2533
2555
  }
2534
2556
 
2535
2557
  export interface PreparedExtractContext {
2558
+ readonly materialContext?: MaterialCookProgramContext | undefined;
2536
2559
  readonly assets: AssetRegistry | null | undefined;
2537
2560
  readonly pipelineState: ExtractPipelineSurface | null | undefined;
2538
2561
  readonly materialSnapshotCache: MaterialSnapshotCache | undefined;
@@ -2542,11 +2565,17 @@ export interface PreparedExtractContext {
2542
2565
  readonly cull: 'self' | 'none' | 'external';
2543
2566
  readonly cullCameras: readonly CameraSnapshot[] | undefined;
2544
2567
  readonly renderables: 'full' | 'none';
2568
+ /** Restrict renderable extraction to these entities while still reading frame resources. */
2569
+ readonly renderableEntities: ReadonlySet<number> | undefined;
2570
+ /** Keep authored-hidden renderables in the persistent source projection. */
2571
+ readonly retainHidden: boolean;
2545
2572
  readonly visibility: VisibilitySnapshot;
2546
2573
  /** Producer-owned artifact lookup; extraction only, never record-time asset reads. */
2547
2574
  readonly getMaterialShaderArtifact:
2548
2575
  | ((materialShaderId: string) => MaterialShaderArtifact | undefined)
2549
2576
  | undefined;
2577
+ /** Renderer-owned canonical source for explicit large instance collections. */
2578
+ readonly instanceCollections: InstanceProjectionStore | undefined;
2550
2579
  }
2551
2580
 
2552
2581
  const ALL_VISIBLE: VisibilitySnapshot = {
@@ -2727,6 +2756,7 @@ export function tonemapParams(camera: CameraSnapshot): Uint8Array {
2727
2756
  export function prepareExtractContext(
2728
2757
  world: World,
2729
2758
  options: {
2759
+ readonly materialContext?: MaterialCookProgramContext;
2730
2760
  readonly assets?: AssetRegistry | null;
2731
2761
  readonly pipelineState?: ExtractPipelineSurface | null;
2732
2762
  readonly materialSnapshotCache?: MaterialSnapshotCache;
@@ -2734,13 +2764,17 @@ export function prepareExtractContext(
2734
2764
  readonly cull?: 'self' | 'none' | 'external';
2735
2765
  readonly cullCameras?: readonly CameraSnapshot[];
2736
2766
  readonly renderables?: 'full' | 'none';
2767
+ readonly renderableEntities?: ReadonlySet<number>;
2768
+ readonly retainHidden?: boolean;
2737
2769
  readonly getMaterialShaderArtifact?: (
2738
2770
  materialShaderId: string,
2739
2771
  ) => MaterialShaderArtifact | undefined;
2772
+ readonly instanceCollections?: InstanceProjectionStore;
2740
2773
  } = {},
2741
2774
  ): PreparedExtractContext {
2742
2775
  const renderables = options.renderables ?? 'full';
2743
2776
  return {
2777
+ materialContext: options.materialContext,
2744
2778
  assets: options.assets,
2745
2779
  pipelineState: options.pipelineState,
2746
2780
  materialSnapshotCache: options.materialSnapshotCache,
@@ -2748,8 +2782,11 @@ export function prepareExtractContext(
2748
2782
  cull: options.cull ?? 'self',
2749
2783
  cullCameras: options.cullCameras,
2750
2784
  renderables,
2785
+ renderableEntities: options.renderableEntities,
2786
+ retainHidden: options.retainHidden ?? false,
2751
2787
  visibility: renderables === 'none' ? ALL_VISIBLE : resolveVisibility(world),
2752
2788
  getMaterialShaderArtifact: options.getMaterialShaderArtifact,
2789
+ instanceCollections: options.instanceCollections,
2753
2790
  };
2754
2791
  }
2755
2792
 
@@ -118,7 +118,13 @@ import type { EnvironmentInspection } from './environment/inspection';
118
118
  import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_UNIFORM } from './gpu-usage';
119
119
  import { resetHdrpBuffers } from './hdrp-buffers';
120
120
  import { assembleMaterialWithSkylightEntries } from './ibl/skylight-bind-group';
121
- import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
121
+ import {
122
+ disposeInstanceBufferChunks,
123
+ disposeInstanceBuffers,
124
+ disposeTransientInstanceBuffers,
125
+ } from './instance-buffer-cache';
126
+ import type { InstanceCollectionInspection } from './instances';
127
+ import { InstanceProjectionStore } from './instances';
122
128
  import {
123
129
  type MeshMaterialBindingObservation,
124
130
  type MeshMaterialBindingPreparationFailure,
@@ -182,7 +188,11 @@ import type {
182
188
  RenderableSnapshot,
183
189
  } from './render-system-extract';
184
190
  import { extractFrames } from './render-system-extract';
185
- import { type PersistentGpuDrivenCandidate, PersistentRenderScene } from './scene/render-scene';
191
+ import {
192
+ type PersistentGpuDrivenCandidate,
193
+ type PersistentRenderCandidateRequest,
194
+ PersistentRenderScene,
195
+ } from './scene/render-scene';
186
196
  import { inspectLodOcclusion } from './scene/visibility/inspection';
187
197
  import { resolveSsaoParameters } from './ssao-config';
188
198
  import { projectSsrDependencies, resolveSsrAdmissionGeneration } from './ssr/admission';
@@ -660,7 +670,7 @@ export interface RenderSystem {
660
670
  /** Prepare a detached graph candidate without entering the frame record path. */
661
671
  prepareRecoveryGraphCandidate(
662
672
  runtime: RecoveryGraphCandidateRuntime,
663
- ): RecoveryGraphCandidatePreparation;
673
+ ): Promise<RecoveryGraphCandidatePreparation>;
664
674
  /** Finish and submit detached candidate setup, never a frame receipt. */
665
675
  submitCandidateSetup(
666
676
  candidate: RecoveryGraphCandidate,
@@ -686,6 +696,8 @@ export interface RenderSystem {
686
696
  readonly pipelineDispatchCounts: {
687
697
  readonly unlit: number;
688
698
  };
699
+ /** Detached residency and upload facts for explicit instance collections. */
700
+ readonly instanceCollectionsInspection: readonly InstanceCollectionInspection[];
689
701
  /**
690
702
  * feat-20260528-frustum-culling M5 / w14: per-frame frustum-culling counters.
691
703
  * Updated by `draw([world], { cameraOwner: 0, resourceOwner: 0 })` on every call from the Extract stage.
@@ -1071,6 +1083,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1071
1083
  let preparedWorlds: readonly World[] = [];
1072
1084
  let latestCamera: CameraSnapshot | undefined;
1073
1085
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
1086
+ const instanceCollections = new InstanceProjectionStore();
1074
1087
  const reflectionProbeOwner = new ReflectionProbeRecordOwner(internals);
1075
1088
  let ssrFormatReceipt: import('@forgeax/engine-rhi').RhiTextureFormatCapabilityReceipt | undefined;
1076
1089
  let ssrFormatProbeGeneration = -1;
@@ -1112,6 +1125,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1112
1125
  onSharedRefMutation: (worldId, handle) => {
1113
1126
  internals.gpuStore.invalidateMesh(handle, preparedWorlds[worldId] ?? worldId);
1114
1127
  },
1128
+ instanceCollections,
1129
+ getSkinPaletteAllocator: () => internals.getPipelineState()?.skinPaletteAllocator ?? null,
1115
1130
  });
1116
1131
  let environmentLifecycle = new EnvironmentLifecycle(internals.deviceScope);
1117
1132
  const transmissionAdmission = new TransmissionCandidateAdmission();
@@ -1362,6 +1377,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1362
1377
  currentDirectionalShadowView: null,
1363
1378
  currentSpotShadowView: null,
1364
1379
  instanceBuffers: new Map(),
1380
+ instanceResidency: new Map(),
1381
+ instanceBufferChunks: new Map(),
1365
1382
  probeBlendBuffers: new Map(),
1366
1383
  probeBlendRecordBufferCapacity: 0,
1367
1384
  morphBuffers: new Map(),
@@ -2294,6 +2311,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2294
2311
  createPreparedResolverCaches,
2295
2312
  });
2296
2313
  return {
2314
+ get instanceCollectionsInspection(): readonly InstanceCollectionInspection[] {
2315
+ return instanceCollections._inspections(
2316
+ frameState.instanceResidency,
2317
+ frameState.frameNumber - 1,
2318
+ );
2319
+ },
2297
2320
  releaseProfilerCatalog(): void {
2298
2321
  releaseProfilerCatalog?.();
2299
2322
  releaseProfilerCatalog = undefined;
@@ -2555,7 +2578,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2555
2578
  compositionWorlds,
2556
2579
  { cameraOwner, resourceOwner },
2557
2580
  internals.assets.catalogEpoch,
2558
- (renderables) =>
2581
+ (request: PersistentRenderCandidateRequest) =>
2559
2582
  extractFrames(
2560
2583
  compositionWorlds,
2561
2584
  { cameraOwner, resourceOwner },
@@ -2563,8 +2586,32 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2563
2586
  internals.getPipelineState(),
2564
2587
  persistentRenderScene.materialSnapshotCacheStore(),
2565
2588
  {
2589
+ ...(internals.device.caps.backendKind === 'null'
2590
+ ? {}
2591
+ : {
2592
+ materialContext: {
2593
+ backend:
2594
+ internals.device.caps.backendKind === 'wgpu-webgl2'
2595
+ ? ('webgl2' as const)
2596
+ : internals.device.caps.backendKind,
2597
+ capability: internals.device.caps.storageBuffer
2598
+ ? ('storage-buffer' as const)
2599
+ : ('uniform-fallback' as const),
2600
+ pipeline: 'forward' as const,
2601
+ geometry: 'mesh' as const,
2602
+ pass: 'forward' as const,
2603
+ profile: 'forgeax-material-wgsl-v1' as const,
2604
+ toolchain: 'naga-oil' as const,
2605
+ instrumentation: 'none' as const,
2606
+ },
2607
+ }),
2566
2608
  cull: 'none',
2567
- renderables,
2609
+ renderables: request === 'none' ? 'none' : 'full',
2610
+ ...(request === 'none' || request === 'full'
2611
+ ? {}
2612
+ : { renderableEntitiesByWorld: request.entitiesByWorld }),
2613
+ retainHidden: true,
2614
+ instanceCollections,
2568
2615
  ...(internals.getMaterialShaderArtifact === undefined
2569
2616
  ? {}
2570
2617
  : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }),
@@ -3496,6 +3543,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3496
3543
  (internals as TimingSessionHost).gpuPassTimingBeginReason = undefined;
3497
3544
  pointsLinesOwner.dispose();
3498
3545
  persistentRenderScene.dispose();
3546
+ instanceCollections.dispose();
3499
3547
  gpuDrivenProduction.dispose();
3500
3548
  occlusionRuntime?.dispose();
3501
3549
  disposeFeatureGpuWork();
@@ -3565,6 +3613,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3565
3613
  // so destroy failures fire structured errors (unified per-frame +
3566
3614
  // dispose error strategy).
3567
3615
  disposeInstanceBuffers(frameState.instanceBuffers, internals.errorRegistry);
3616
+ frameState.instanceResidency?.clear();
3617
+ if (frameState.instanceBufferChunks !== undefined) {
3618
+ disposeInstanceBufferChunks(frameState.instanceBufferChunks, internals.errorRegistry);
3619
+ }
3568
3620
  disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
3569
3621
  if (frameState.probeBlendRecordBuffer !== undefined) {
3570
3622
  if (!frameState.probeBlendRecordBuffer.isDestroyed) {
@@ -3706,6 +3758,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3706
3758
  }
3707
3759
  frameState.retiredCompiledFrameGraphs.clear();
3708
3760
  frameState.instanceBuffers.clear();
3761
+ frameState.instanceBufferChunks?.clear();
3762
+ frameState.instanceResidency?.clear();
3709
3763
  delete frameState.probeBlendRecordBuffer;
3710
3764
  frameState.probeBlendRecordBufferCapacity = 0;
3711
3765
  frameState.probeBlendBuffers.clear();
@@ -36,6 +36,8 @@ export interface RenderSceneApplyResult {
36
36
  readonly ignoredLateUpdates: number;
37
37
  readonly createdSlots: readonly RenderSceneSlot[];
38
38
  readonly updatedSlots: readonly RenderSceneSlot[];
39
+ /** Full retained-snapshot updates; transform spans stay in updatedSlots only. */
40
+ readonly contentUpdatedSlots?: readonly RenderSceneSlot[];
39
41
  readonly removedSlots: readonly RenderSceneRecord[];
40
42
  readonly recreatedSlots: readonly RenderSceneSlot[];
41
43
  readonly resynced: number;