@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
@@ -12,6 +12,7 @@ import {
12
12
  type MaterialShaderManifestEntry,
13
13
  type ShaderCatalog,
14
14
  type ShaderCatalogDevice,
15
+ STANDARD_PIPELINE_PARAM_SCHEMA,
15
16
  standardPhysicalTextureFields,
16
17
  } from '@forgeax/engine-shader';
17
18
  import type { AssetGuid as AssetGuidType, ParamSchemaEntry } from '@forgeax/engine-types';
@@ -282,11 +283,10 @@ export async function prepareMaterialShaders(
282
283
  const capValue = (rhiDevice.limits as Readonly<Record<string, number>>)
283
284
  .maxStorageBuffersPerShaderStage;
284
285
  const webgl2Downlevel = rhiDevice.caps.backendKind === 'wgpu-webgl2';
285
- let storageBufferCapable = !webgl2Downlevel;
286
- if (typeof capValue === 'number') {
286
+ const storageBufferCapable = rhiDevice.caps.storageBuffer;
287
+ if (storageBufferCapable && typeof capValue === 'number') {
287
288
  const capCheck = assertStorageBufferCap(capValue);
288
289
  if (!capCheck.ok) throw capCheck.error;
289
- storageBufferCapable = !webgl2Downlevel && capCheck.value;
290
290
  }
291
291
  const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
292
292
  const projectorAvailable =
@@ -314,9 +314,10 @@ export async function prepareMaterialShaders(
314
314
  // engine-entry compile path, NOT by installMaterialArtifact.
315
315
  if (msEntry.identifier.startsWith('forgeax::engine-')) continue;
316
316
  // buildVariantKey logic: sorted key=value pairs joined with +; all-true = ''.
317
- const variantDefines: Record<string, boolean> = {
318
- STORAGE_BUFFER_AVAILABLE: storageBufferCapable,
319
- };
317
+ const variantDefines: Record<string, boolean> = {};
318
+ if (msEntry.variants.some((variant) => 'STORAGE_BUFFER_AVAILABLE' in variant.defines)) {
319
+ variantDefines.STORAGE_BUFFER_AVAILABLE = storageBufferCapable;
320
+ }
320
321
  // User material manifests may declare WEBGL2_COMPAT alongside the shared
321
322
  // mesh-storage axis. The fallback backend must select that authored
322
323
  // branch at boot; otherwise it asks for the impossible bare
@@ -450,11 +451,12 @@ export async function prepareMaterialShaders(
450
451
  (candidate) => candidate.identifier === shadowCasterIdentifier,
451
452
  );
452
453
  if (manifestEntry !== undefined) {
453
- // shadow_caster has no authored material parameters. M3 / w12-w13
454
- // (D-2 / D-12): derive([]) is the graceful empty-schema path.
454
+ // shadow_caster evaluates the same default Surface as Standard. Keep
455
+ // its generated MaterialParameters contract aligned with the Standard
456
+ // pipeline even when a legacy manifest omitted the material entry.
455
457
  registry.installMaterialArtifact(shadowCasterIdentifier, {
456
458
  source: manifestEntry.composedWgsl,
457
- paramSchema: [],
459
+ paramSchema: STANDARD_PIPELINE_PARAM_SCHEMA,
458
460
  });
459
461
  const shaderGuid = ENGINE_SHADER_GUIDS.get(shadowCasterIdentifier);
460
462
  if (shaderGuid !== undefined) {
@@ -462,7 +464,7 @@ export async function prepareMaterialShaders(
462
464
  kind: 'shader' as const,
463
465
  name: shadowCasterIdentifier,
464
466
  source: manifestEntry.composedWgsl,
465
- paramSchema: [] as readonly import('@forgeax/engine-types').ParamSchemaEntry[],
467
+ paramSchema: STANDARD_PIPELINE_PARAM_SCHEMA,
466
468
  };
467
469
  // feat-20260614 M8 (D-17): catalogue by GUID; no handle minted.
468
470
  assets.catalog(shaderGuid, shaderAsset);
@@ -659,6 +661,82 @@ export function resolveMaterialShaderVertexInputContract(
659
661
  : 'none';
660
662
  }
661
663
 
664
+ /**
665
+ * Resolve the number of UV sets declared by one material artifact.
666
+ *
667
+ * Runtime-published Surface programs do not necessarily have a manifest
668
+ * reflection row, so the authored vertex input is the fallback source of
669
+ * truth. The renderer then feeds this count into the geometry alias
670
+ * projection, which clamps missing mesh UV sets to the last available set.
671
+ */
672
+ export function resolveMaterialShaderUvSetCount(
673
+ source: string | undefined,
674
+ reflectedCount?: number,
675
+ ): number | undefined {
676
+ if (reflectedCount !== undefined) return reflectedCount;
677
+ if (source === undefined) return undefined;
678
+
679
+ const withoutComments = source.replace(/\/\*[\s\S]*?\*\//gu, '').replace(/\/\/.*$/gmu, '');
680
+ const inputStructs = [
681
+ ...withoutComments.matchAll(/struct\s+([A-Za-z_]\w*)\s*\{([^}]*)\}/gu),
682
+ ].filter((match) => /@location\s*\(/u.test(match[2] ?? ''));
683
+ for (const inputStruct of inputStructs) {
684
+ const name = inputStruct[1];
685
+ const body = inputStruct[2];
686
+ if (name === undefined || body === undefined) continue;
687
+ const firstParameter = new RegExp(
688
+ `@vertex\\s*fn\\s+\\w+\\s*\\(\\s*\\w+\\s*:\\s*${name}\\b`,
689
+ 'u',
690
+ );
691
+ if (!firstParameter.test(withoutComments)) continue;
692
+ let uvSetCount = 0;
693
+ for (const location of body.matchAll(/@location\s*\(\s*(\d+)\s*\)\s+([A-Za-z_]\w*)/gu)) {
694
+ const field = location[2];
695
+ if (field === 'uv') {
696
+ uvSetCount = Math.max(uvSetCount, 1);
697
+ continue;
698
+ }
699
+ const uvIndex = field?.match(/^uv(\d+)_?$/u)?.[1];
700
+ if (uvIndex !== undefined) uvSetCount = Math.max(uvSetCount, Number(uvIndex) + 1);
701
+ }
702
+ return uvSetCount > 0 ? uvSetCount : undefined;
703
+ }
704
+ return undefined;
705
+ }
706
+
707
+ /**
708
+ * Select a published material artifact that matches a downlevel backend.
709
+ *
710
+ * Cooked Surface publications carry immutable backend variants in their
711
+ * metadata. WebGL2 must select the uniform-fallback specialization before
712
+ * pipeline construction so its shader, bind-group layout, and cache key stay
713
+ * capability-consistent.
714
+ */
715
+ export function resolveMaterialShaderBackendArtifactKey(
716
+ materialShaderId: string,
717
+ backendKind: 'webgpu' | 'wgpu-native' | 'wgpu-webgl2' | 'null',
718
+ artifact:
719
+ | {
720
+ readonly metadata?: Readonly<Record<string, unknown>>;
721
+ }
722
+ | undefined,
723
+ ): string {
724
+ if (backendKind !== 'wgpu-webgl2') return materialShaderId;
725
+ const variants = artifact?.metadata?.variants;
726
+ if (!Array.isArray(variants)) return materialShaderId;
727
+ const fallback = variants.find((variant): variant is { specializationKey: string } => {
728
+ if (typeof variant !== 'object' || variant === null) return false;
729
+ const candidate = variant as Record<string, unknown>;
730
+ return (
731
+ candidate.backend === 'webgl2' &&
732
+ candidate.capability === 'uniform-fallback' &&
733
+ typeof candidate.specializationKey === 'string' &&
734
+ candidate.specializationKey.length > 0
735
+ );
736
+ });
737
+ return fallback?.specializationKey ?? materialShaderId;
738
+ }
739
+
662
740
  export function allowsUnlitPreparedFallback(
663
741
  depthFormatOverride: GPUTextureFormat | null | undefined,
664
742
  vertexLayout: string | undefined,
@@ -632,7 +632,7 @@ export function createRendererRecovery(deps: RendererRecoveryDependencies): Rend
632
632
  releaseCandidate();
633
633
  return err(new RecoverError('recover-device-unavailable'));
634
634
  }
635
- const graphPreparation = renderSystem.prepareRecoveryGraphCandidate({
635
+ const graphPreparation = await renderSystem.prepareRecoveryGraphCandidate({
636
636
  internals: candidateInternals,
637
637
  pipelineState: candidatePipelineState,
638
638
  });
@@ -54,7 +54,6 @@ import {
54
54
  } from '../ibl/skylight-bind-group';
55
55
  import type { BloomInspection } from '../inspection-types';
56
56
  import type { DeviceScope, RhiErrorListenerRegistry } from '../lifecycle';
57
- import { assertStorageBufferCap } from '../light-buffer-layout';
58
57
  import {
59
58
  buildPbrPipelineLayouts,
60
59
  buildPbrSkinLayouts,
@@ -218,22 +217,10 @@ export async function buildReadyWebGPU(
218
217
  setVolumetricFogShaderSources: (sources: VolumetricFogShaderSources) => void,
219
218
  ): Promise<PipelineState> {
220
219
  const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
221
- // bug-20260610: WebGL2 fallback path read storage-buffer capability up
222
- // front so both Step 2 (shader-module compile, where we patch engine
223
- // entries to the STORAGE_BUFFER_AVAILABLE=false variant) and Step 3
224
- // (buildPbrPipelineLayouts, which switches between read-only-storage
225
- // and uniform BGL entries) walk the same axis. Step 0 already ran in
226
- // `prepareMaterialShaders`; this is an idempotent re-read driven by
227
- // `device.limits.maxStorageBuffersPerShaderStage`, which is 0 on the
228
- // wgpu-wasm WebGL2 backend (downlevel_webgl2_defaults).
229
- const capValue = (rhiDevice.limits as Readonly<Record<string, number>>)
230
- .maxStorageBuffersPerShaderStage;
220
+ // Shader layouts and record-stage buffer usage share the RHI capability.
221
+ // Backend names and numeric limits must not independently re-enable storage.
222
+ const storageBufferCapable = rhiDevice.caps.storageBuffer;
231
223
  const webgl2Downlevel = rhiDevice.caps.backendKind === 'wgpu-webgl2';
232
- let storageBufferCapable = !webgl2Downlevel;
233
- if (typeof capValue === 'number') {
234
- const capCheck = assertStorageBufferCap(capValue);
235
- storageBufferCapable = capCheck.ok ? !webgl2Downlevel && capCheck.value : !webgl2Downlevel;
236
- }
237
224
  const directionalPcssAvailable =
238
225
  rhiDevice.caps.backendKind === 'webgpu' || rhiDevice.caps.backendKind === 'wgpu-native';
239
226
  const projectorAvailable =
@@ -30,7 +30,6 @@ import { createRenderReadLease, type RenderReadLease } from '@forgeax/engine-ecs
30
30
  import {
31
31
  deriveVertexBufferLayout,
32
32
  deriveVertexBufferLayoutFromProjection,
33
- type GpuVertexBufferLayoutEntry,
34
33
  type VertexLayoutProjection,
35
34
  } from '@forgeax/engine-geometry';
36
35
  import type {
@@ -176,7 +175,9 @@ import {
176
175
  makeShaderDeviceAdapter,
177
176
  normalizeMaterialShaderVariantSet,
178
177
  prepareMaterialShaders,
178
+ resolveMaterialShaderBackendArtifactKey,
179
179
  resolveMaterialShaderBindingContract,
180
+ resolveMaterialShaderUvSetCount,
180
181
  resolveMaterialShaderVariantSet,
181
182
  resolveMaterialShaderVertexInputContract,
182
183
  type ShaderDeviceAdapterInternal,
@@ -214,14 +215,20 @@ import {
214
215
  import { STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES } from './shader-prewarm-policy';
215
216
  import { buildReadyWebGPU } from './webgpu-ready';
216
217
  import { DEPTH_TEXTURE_FORMAT, HDR_COLOR_ATTACHMENT_FORMAT } from './webgpu-ready-contract';
218
+ import {
219
+ BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS,
220
+ DEFAULT_VERTEX_ATTRS,
221
+ MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS,
222
+ PBR_SKIN_SENTINEL_ATTR_BUFFER,
223
+ POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS,
224
+ PREPARED_INSTANCE_VERTEX_ATTRS,
225
+ PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS,
226
+ TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS,
227
+ toGpuVertexBufferLayouts,
228
+ } from './webgpu-vertex-layouts';
217
229
 
218
230
  export type { BundlerOptions } from './bundler-contract';
219
231
  export { assembleMaterialProjection } from './material/assembly';
220
- export {
221
- projectMaterialPipeline,
222
- routeMaterialPipeline,
223
- updateMaterialRuntimeValues,
224
- } from './material/pipeline-projection';
225
232
  export type {
226
233
  LayoutKind,
227
234
  MaterialShaderBindingContract,
@@ -231,7 +238,9 @@ export {
231
238
  allowsUnlitPreparedFallback,
232
239
  isSharedMaterialUserRegionCompatible,
233
240
  normalizeMaterialShaderVariantSet,
241
+ resolveMaterialShaderBackendArtifactKey,
234
242
  resolveMaterialShaderBindingContract,
243
+ resolveMaterialShaderUvSetCount,
235
244
  resolveMaterialShaderVariantSet,
236
245
  resolveMaterialShaderVertexInputContract,
237
246
  selectNoColorPbrVariant,
@@ -273,139 +282,6 @@ function adaptMipmapShaderModuleFactory(
273
282
  // Transform.world mat4 derivation (audio listener sync + picking read the
274
283
  // derived Transform.world from scripts).
275
284
 
276
- // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4): a single
277
- // shared empty ArrayBuffer used by `buildPipelineContext` to synthesize the
278
- // 6-key VertexAttributeMap when no caller-supplied attributes are available
279
- // for the pbr-skin path. `deriveVertexBufferLayout` is the SSOT for the layout
280
- // (vertex-attribute-layout.ts) and reads only key presence -- value identity
281
- // never matters -- so a zero-byte ArrayBuffer per key produces the exact
282
- // 6-attribute / 72-byte stride layout that the @forgeax::pbr-skin shader's
283
- // @location(0..5) declarations expect.
284
- const PBR_SKIN_SENTINEL_ATTR_BUFFER = new ArrayBuffer(0);
285
-
286
- /** Default 4-attribute vertex layout used as fallback when meshAttributes is undefined. */
287
- const DEFAULT_VERTEX_ATTRS: VertexAttributeMap = {
288
- position: new Float32Array(0),
289
- normal: new Float32Array(0),
290
- uv: new Float32Array(0),
291
- tangent: new Float32Array(0),
292
- };
293
-
294
- const PREPARED_INSTANCE_VERTEX_ATTRS: VertexAttributeMap = {
295
- position: new Float32Array(0),
296
- normal: new Float32Array(0),
297
- uv: new Float32Array(0),
298
- tangent: new Float32Array(0),
299
- uv1: new Float32Array(0),
300
- };
301
-
302
- const PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS: VertexAttributeMap = {
303
- position: new Float32Array(0),
304
- normal: new Float32Array(0),
305
- uv: new Float32Array(0),
306
- tangent: new Float32Array(0),
307
- skinIndex: new Uint16Array(0),
308
- skinWeight: new Float32Array(0),
309
- };
310
-
311
- const POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS = [
312
- {
313
- arrayStride: 9 * 4,
314
- stepMode: 'instance' as const,
315
- attributes: [
316
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
317
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x2' as const },
318
- { shaderLocation: 2, offset: 5 * 4, format: 'float32x4' as const },
319
- ],
320
- },
321
- ] satisfies readonly GPUVertexBufferLayout[];
322
-
323
- const BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS = [
324
- {
325
- arrayStride: 31 * 4,
326
- stepMode: 'instance' as const,
327
- attributes: [
328
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
329
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x2' as const },
330
- { shaderLocation: 2, offset: 5 * 4, format: 'float32x2' as const },
331
- { shaderLocation: 3, offset: 7 * 4, format: 'float32x4' as const },
332
- { shaderLocation: 4, offset: 11 * 4, format: 'float32x4' as const },
333
- { shaderLocation: 5, offset: 15 * 4, format: 'float32x4' as const },
334
- { shaderLocation: 6, offset: 19 * 4, format: 'float32x4' as const },
335
- { shaderLocation: 7, offset: 23 * 4, format: 'float32x4' as const },
336
- { shaderLocation: 8, offset: 27 * 4, format: 'float32x4' as const },
337
- ],
338
- },
339
- ] satisfies readonly GPUVertexBufferLayout[];
340
-
341
- const TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS = [
342
- {
343
- arrayStride: 12 * 4,
344
- stepMode: 'instance' as const,
345
- attributes: [
346
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
347
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
348
- { shaderLocation: 2, offset: 6 * 4, format: 'float32x4' as const },
349
- { shaderLocation: 3, offset: 10 * 4, format: 'float32x2' as const },
350
- ],
351
- },
352
- ] satisfies readonly GPUVertexBufferLayout[];
353
-
354
- const MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS = [
355
- {
356
- arrayStride: 12 * 4,
357
- stepMode: 'vertex' as const,
358
- attributes: [
359
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
360
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
361
- { shaderLocation: 2, offset: 6 * 4, format: 'float32x2' as const },
362
- { shaderLocation: 3, offset: 8 * 4, format: 'float32x4' as const },
363
- ],
364
- },
365
- {
366
- arrayStride: 28 * 4,
367
- stepMode: 'instance' as const,
368
- attributes: [
369
- { shaderLocation: 4, offset: 0, format: 'float32x3' as const },
370
- { shaderLocation: 5, offset: 3 * 4, format: 'float32x3' as const },
371
- { shaderLocation: 6, offset: 6 * 4, format: 'float32x3' as const },
372
- { shaderLocation: 7, offset: 9 * 4, format: 'float32x3' as const },
373
- { shaderLocation: 8, offset: 12 * 4, format: 'float32x4' as const },
374
- { shaderLocation: 9, offset: 16 * 4, format: 'float32x4' as const },
375
- { shaderLocation: 10, offset: 20 * 4, format: 'float32x4' as const },
376
- { shaderLocation: 11, offset: 24 * 4, format: 'float32x4' as const },
377
- ],
378
- },
379
- ] satisfies readonly GPUVertexBufferLayout[];
380
-
381
- function toGpuVertexFormat(
382
- format: GpuVertexBufferLayoutEntry['attributes'][number]['format'],
383
- ): GPUVertexFormat {
384
- switch (format) {
385
- case 'float32x2':
386
- case 'float32x3':
387
- case 'float32x4':
388
- case 'uint16x4':
389
- return format;
390
- default:
391
- throw new Error(`Unsupported geometry vertex format: ${format}`);
392
- }
393
- }
394
-
395
- function toGpuVertexBufferLayouts(
396
- entries: readonly GpuVertexBufferLayoutEntry[],
397
- ): readonly GPUVertexBufferLayout[] {
398
- return entries.map((entry) => ({
399
- arrayStride: entry.arrayStride,
400
- ...(entry.stepMode === undefined ? {} : { stepMode: entry.stepMode }),
401
- attributes: entry.attributes.map((attribute) => ({
402
- shaderLocation: attribute.shaderLocation,
403
- offset: attribute.offset,
404
- format: toGpuVertexFormat(attribute.format),
405
- })),
406
- }));
407
- }
408
-
409
285
  /**
410
286
  * Bundler-layer injection accepted by `createRenderer` (and proxied by
411
287
  * `createApp`) as the optional third argument.
@@ -1954,15 +1830,19 @@ async function makeWebGPURenderer(
1954
1830
  vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance)
1955
1831
  ? getOrBuildPreparedMaterialPipelineLayout(materialShaderId)
1956
1832
  : null;
1833
+ const materialShaderLookup =
1834
+ materialShaderId === undefined
1835
+ ? undefined
1836
+ : getShader().findMaterialArtifact(materialShaderId);
1837
+ const materialShaderSource = materialShaderLookup?.ok
1838
+ ? materialShaderLookup.value.source
1839
+ : undefined;
1957
1840
  const vertexInputContract =
1958
1841
  materialShaderId === undefined
1959
1842
  ? 'render-material'
1960
- : (() => {
1961
- const lookup = getShader().findMaterialArtifact(materialShaderId);
1962
- return lookup.ok
1963
- ? resolveMaterialShaderVertexInputContract(lookup.value.source)
1964
- : 'render-material';
1965
- })();
1843
+ : materialShaderSource !== undefined
1844
+ ? resolveMaterialShaderVertexInputContract(materialShaderSource)
1845
+ : 'render-material';
1966
1846
  const currentState = currentPipelineState();
1967
1847
  const pipelineLayout =
1968
1848
  group0Layout !== null
@@ -1979,9 +1859,16 @@ async function makeWebGPURenderer(
1979
1859
  ? perShaderLayout.pipelineLayout
1980
1860
  : selectPipelineLayoutForVariant(currentState, variantSet, layoutKind);
1981
1861
  if (pipelineLayout === null) return null;
1862
+ const currentMaterialUvSetCounts = currentMaterialShaderUvSetCounts();
1982
1863
  const resolvedUvSetCount =
1983
1864
  materialShaderId !== undefined
1984
- ? currentMaterialShaderUvSetCounts().get(materialShaderId)
1865
+ ? (currentMaterialUvSetCounts.get(materialShaderId) ??
1866
+ (materialShaderSource !== undefined
1867
+ ? resolveMaterialShaderUvSetCount(
1868
+ materialShaderSource,
1869
+ currentMaterialUvSetCounts.get(materialShaderId),
1870
+ )
1871
+ : undefined))
1985
1872
  : undefined;
1986
1873
  const vertexBuffers: readonly GPUVertexBufferLayout[] =
1987
1874
  vertexInputContract === 'none'
@@ -2110,6 +1997,8 @@ async function makeWebGPURenderer(
2110
1997
  shaderModuleMode?: RenderFeatureShaderModuleMode,
2111
1998
  layoutKindOverride?: LayoutKind,
2112
1999
  additionalColorFormats?: readonly GPUTextureFormat[],
2000
+ vertexEntry?: string,
2001
+ authoredFragmentEntry?: string,
2113
2002
  ): RenderPipeline | null => {
2114
2003
  const group2Contract = resolvePipelineGroup2Contract(entry.source);
2115
2004
  const ctx = buildPipelineContext(
@@ -2132,10 +2021,11 @@ async function makeWebGPURenderer(
2132
2021
  if (currentState === null) return null;
2133
2022
  const ldrColorFormat = colorFormatOverride ?? currentState.colorAttachmentFormat;
2134
2023
  const fragmentEntry =
2135
- isHdr &&
2024
+ authoredFragmentEntry ??
2025
+ (isHdr &&
2136
2026
  (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
2137
2027
  ? 'fs_main_hdr'
2138
- : undefined;
2028
+ : undefined);
2139
2029
  const built = buildPipelineForMaterialShader(
2140
2030
  cacheKey,
2141
2031
  // The catalog entry is projected into the material-shader entry shape.
@@ -2172,7 +2062,7 @@ async function makeWebGPURenderer(
2172
2062
  : {}),
2173
2063
  }
2174
2064
  : undefined,
2175
- undefined, // vertexEntry — default vs_main
2065
+ vertexEntry,
2176
2066
  fragmentEntry,
2177
2067
  undefined, // defines — none
2178
2068
  passKind,
@@ -2239,6 +2129,8 @@ async function makeWebGPURenderer(
2239
2129
  shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
2240
2130
  layoutKindOverride?: LayoutKind,
2241
2131
  additionalColorFormats?: readonly GPUTextureFormat[],
2132
+ vertexEntry?: string,
2133
+ fragmentEntry?: string,
2242
2134
  ): RenderPipeline | null => {
2243
2135
  // WebGL2 is a downlevel backend even when the record stage asks for the
2244
2136
  // native-capability variant string. Resolve the variant axes at this
@@ -2252,15 +2144,31 @@ async function makeWebGPURenderer(
2252
2144
  (currentState !== null ? currentState.colorAttachmentFormat : 'bgra8unorm-srgb');
2253
2145
  // feat-20260629 M4: auto-fill shaderUvSetCount from naga reflection
2254
2146
  // (stored in materialShaderUvSetCounts during prepareMaterialShaders).
2255
- const resolvedUvSetCount =
2256
- shaderUvSetCount ?? currentMaterialShaderUvSetCounts().get(materialShaderId);
2257
- const manifestEntry = findMaterialShaderManifestEntry(materialShaderId);
2147
+ const pipelineShaderId = resolveMaterialShaderBackendArtifactKey(
2148
+ materialShaderId,
2149
+ internals.device.caps.backendKind,
2150
+ assets.getMaterialArtifact(materialShaderId),
2151
+ );
2152
+ const materialShaderLookup = getShader().findMaterialArtifact(pipelineShaderId);
2153
+ const materialShaderSource = materialShaderLookup.ok
2154
+ ? materialShaderLookup.value.source
2155
+ : undefined;
2156
+ const currentMaterialUvSetCounts = currentMaterialShaderUvSetCounts();
2157
+ const materialUvSetCount = resolveMaterialShaderUvSetCount(
2158
+ materialShaderSource,
2159
+ currentMaterialUvSetCounts.get(pipelineShaderId),
2160
+ );
2161
+ if (materialUvSetCount !== undefined) {
2162
+ currentMaterialUvSetCounts.set(pipelineShaderId, materialUvSetCount);
2163
+ }
2164
+ const resolvedUvSetCount = shaderUvSetCount ?? materialUvSetCount;
2165
+ const manifestEntry = findMaterialShaderManifestEntry(pipelineShaderId);
2258
2166
  // Sprite's omitted request is the boot-selected default artifact. The
2259
2167
  // explicit empty key remains the PER_INSTANCE_REGION=true request used by
2260
2168
  // SpriteInstances, so only that path is capability-rewritten here.
2261
2169
  const resolvedVariantSet =
2262
2170
  variantSet === undefined &&
2263
- (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
2171
+ (pipelineShaderId === 'forgeax::sprite' || pipelineShaderId === 'forgeax::sprite-lit')
2264
2172
  ? undefined
2265
2173
  : resolveCachedMaterialShaderVariantSet(variantSet, manifestEntry);
2266
2174
  // The record stage also composes engine capability axes for user shaders.
@@ -2272,6 +2180,12 @@ async function makeWebGPURenderer(
2272
2180
  // A single-source manifest entry must stay on its canonical module key;
2273
2181
  // otherwise a synthesized capability axis races the prewarmed module.
2274
2182
  let effectiveVariantSet = normalizeMaterialShaderVariantSet(resolvedVariantSet, manifestEntry);
2183
+ // Runtime-published authored materials are installed from their immutable
2184
+ // cooked artifact, not from the build-time material-shader manifest. The
2185
+ // published artifact identity already selects the backend-specific bytes;
2186
+ // an absent manifest therefore means the source itself is the canonical
2187
+ // pipeline module and must not carry a synthesized capability key.
2188
+ if (manifestEntry === undefined) effectiveVariantSet = undefined;
2275
2189
  if (
2276
2190
  vertexLayoutProjection === undefined &&
2277
2191
  manifestEntry?.variants.some((variant) => 'VERTEX_COLOR_AVAILABLE' in variant.defines)
@@ -2294,7 +2208,7 @@ async function makeWebGPURenderer(
2294
2208
  }
2295
2209
  if (
2296
2210
  manifestEntry !== undefined &&
2297
- !materialShaderId.startsWith('forgeax::') &&
2211
+ !pipelineShaderId.startsWith('forgeax::') &&
2298
2212
  effectiveVariantSet !== undefined &&
2299
2213
  effectiveVariantSet !== ''
2300
2214
  ) {
@@ -2323,7 +2237,13 @@ async function makeWebGPURenderer(
2323
2237
  const colorFormat: GPUTextureFormat = isHdr ? 'rgba16float' : ldrColorFormat;
2324
2238
  const isDepthOnlyPass = passKindPolicyTable[passKind]?.shape === 'depth-only';
2325
2239
  const spec: PipelineSpec = {
2326
- shader: { id: materialShaderId, passKind, variantSet: effectiveVariantSet },
2240
+ shader: {
2241
+ id: pipelineShaderId,
2242
+ passKind,
2243
+ variantSet: effectiveVariantSet,
2244
+ ...(vertexEntry === undefined ? {} : { vertexEntry }),
2245
+ ...(fragmentEntry === undefined ? {} : { fragmentEntry }),
2246
+ },
2327
2247
  attachments: {
2328
2248
  colorFormats:
2329
2249
  additionalColorFormats === undefined
@@ -2362,7 +2282,7 @@ async function makeWebGPURenderer(
2362
2282
  const cached = currentPipelineCacheState().materialShaderPipelineCache.get(cacheKey);
2363
2283
  if (cached !== undefined) return cached;
2364
2284
  if (pipelineState === null) return null;
2365
- const lookup = getShader().findMaterialArtifact(materialShaderId);
2285
+ const lookup = getShader().findMaterialArtifact(pipelineShaderId);
2366
2286
  if (!lookup.ok) {
2367
2287
  if (shouldDeferMissingPreparedMaterialShader(vertexLayout)) {
2368
2288
  // Prepared VFX draws bind only their feature-owned groups. The
@@ -2377,7 +2297,7 @@ async function makeWebGPURenderer(
2377
2297
  // the standard pipeline from the engine-shipped PBR entry.
2378
2298
  if (
2379
2299
  renderState === undefined &&
2380
- !allowsUnlitPreparedFallback(depthFormatOverride, vertexLayout, materialShaderId)
2300
+ !allowsUnlitPreparedFallback(depthFormatOverride, vertexLayout, pipelineShaderId)
2381
2301
  ) {
2382
2302
  // A prepared feature declares the vertex layout it owns. Falling back
2383
2303
  // to default-unlit here would pair that data with the unlit shader's
@@ -2392,7 +2312,7 @@ async function makeWebGPURenderer(
2392
2312
  return buildAndCachePipeline(
2393
2313
  cacheKey,
2394
2314
  unlitLookup.value,
2395
- `pbr-pipeline-prepared-${materialShaderId}`,
2315
+ `pbr-pipeline-prepared-${pipelineShaderId}`,
2396
2316
  // Reuse the eagerly compiled engine unlit module. The adapter cache
2397
2317
  // is seeded under this label during renderer readiness; a new label
2398
2318
  // would keep color-only prepared passes in perpetual warm-up.
@@ -2420,7 +2340,7 @@ async function makeWebGPURenderer(
2420
2340
  return buildAndCachePipeline(
2421
2341
  cacheKey,
2422
2342
  { source: pbrEntry.wgsl, paramSchema: [] },
2423
- `pbr-pipeline-fallback-${materialShaderId}${isHdr ? '-hdr' : ''}`,
2343
+ `pbr-pipeline-fallback-${pipelineShaderId}${isHdr ? '-hdr' : ''}`,
2424
2344
  // w16-b: module identity is the fallback PBR source, stable across
2425
2345
  // topology / renderState / HDR (all baked into the PSO) so every
2426
2346
  // variant reuses one compiled module.
@@ -2454,8 +2374,8 @@ async function makeWebGPURenderer(
2454
2374
  // layout resolution so buildPipelineContext derives the layout that
2455
2375
  // matches the compiled module. Skin keeps its
2456
2376
  // own id so the pbr-skin fail-fast (null layout) is preserved.
2457
- materialShaderId === SKIN_MATERIAL_SHADER_ID
2458
- ? materialShaderId
2377
+ pipelineShaderId === SKIN_MATERIAL_SHADER_ID
2378
+ ? pipelineShaderId
2459
2379
  : 'forgeax::default-standard-pbr',
2460
2380
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
2461
2381
  // forward meshAttributes through the fallback path so the pbr-skin
@@ -2489,7 +2409,7 @@ async function makeWebGPURenderer(
2489
2409
  if (effectiveVariantSet !== undefined) {
2490
2410
  const registry = getShader();
2491
2411
  for (const msEntry of registry.materialShaderManifestEntries()) {
2492
- if (msEntry.identifier === materialShaderId) {
2412
+ if (msEntry.identifier === pipelineShaderId) {
2493
2413
  const variant = findVariantByKey(msEntry, effectiveVariantSet);
2494
2414
  if (variant) {
2495
2415
  // M3 / w12-w13: variant substitution carries the same source +
@@ -2516,7 +2436,7 @@ async function makeWebGPURenderer(
2516
2436
  return buildAndCachePipeline(
2517
2437
  cacheKey,
2518
2438
  shaderEntry,
2519
- `pbr-pipeline-${materialShaderId}${isHdr ? '-hdr' : ''}${passKindLabelSegment}`,
2439
+ `pbr-pipeline-${pipelineShaderId}${isHdr ? '-hdr' : ''}${passKindLabelSegment}`,
2520
2440
  // feat-20260609 M4 / w31: when variantSet is non-empty, the module identity
2521
2441
  // includes the variant key so URP (STORAGE_BUFFER_AVAILABLE=true) and HDRP
2522
2442
  // (CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true) variants
@@ -2530,8 +2450,8 @@ async function makeWebGPURenderer(
2530
2450
  // empty-tail segment); module identity stays a function of the
2531
2451
  // exact variantSet string.
2532
2452
  effectiveVariantSet !== undefined
2533
- ? `module-${materialShaderId}#${effectiveVariantSet}`
2534
- : `module-${materialShaderId}`,
2453
+ ? `module-${pipelineShaderId}#${effectiveVariantSet}`
2454
+ : `module-${pipelineShaderId}`,
2535
2455
  isHdr,
2536
2456
  renderState,
2537
2457
  topology,
@@ -2547,7 +2467,7 @@ async function makeWebGPURenderer(
2547
2467
  // bug-20260611-skin-pipeline-layout: thread the registered materialShaderId
2548
2468
  // so buildPipelineContext can resolve LayoutKind='pbr-skin' for the skin
2549
2469
  // shader (2-entry mesh-array BGL).
2550
- materialShaderId,
2470
+ pipelineShaderId,
2551
2471
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
2552
2472
  // forward meshAttributes so the pbr-skin path's vertex buffer layout
2553
2473
  // is derived via the SSOT (deriveVertexBufferLayout). For URP/HDRP
@@ -2562,6 +2482,8 @@ async function makeWebGPURenderer(
2562
2482
  shaderModuleMode,
2563
2483
  layoutKindOverride,
2564
2484
  additionalColorFormats,
2485
+ vertexEntry,
2486
+ fragmentEntry,
2565
2487
  );
2566
2488
  };
2567
2489
  const getMaterialShaderPipelineEntry = (
@@ -3009,6 +2931,24 @@ async function makeWebGPURenderer(
3009
2931
  featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
3010
2932
  frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
3011
2933
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
2934
+ instanceCollections: Object.freeze(
2935
+ renderSystem.instanceCollectionsInspection.map((collection) =>
2936
+ Object.freeze({
2937
+ ...collection,
2938
+ uploadRanges: Object.freeze(
2939
+ collection.uploadRanges.map((range) => Object.freeze({ ...range })),
2940
+ ),
2941
+ ...(collection.error === undefined
2942
+ ? {}
2943
+ : {
2944
+ error: Object.freeze({
2945
+ ...collection.error,
2946
+ detail: Object.freeze({ ...collection.error.detail }),
2947
+ }),
2948
+ }),
2949
+ }),
2950
+ ),
2951
+ ),
3012
2952
  renderScene: renderSystem.renderScene,
3013
2953
  reflectionProbes: renderSystem.reflectionProbes,
3014
2954
  ssrDependencies: renderSystem.ssrDependencies,