@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 { probeVideoHighPerfUpload } from '@forgeax/engine-graphics-extras';
1
2
  import {
2
3
  type BindGroup,
3
4
  type BindGroupEntry,
@@ -8,14 +9,8 @@ import {
8
9
  type RhiRenderPassEncoder,
9
10
  } from '@forgeax/engine-rhi';
10
11
  import type { MaterialRenderState, PassSelector } from '@forgeax/engine-types';
11
- import { GpuBuffer } from '../gpu-resource';
12
- import {
13
- GPU_BUFFER_USAGE_COPY_DST,
14
- GPU_BUFFER_USAGE_STORAGE,
15
- GPU_BUFFER_USAGE_UNIFORM,
16
- } from '../gpu-usage';
12
+ import type { GpuBuffer } from '../gpu-resource';
17
13
  import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
18
- import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
19
14
  import {
20
15
  buildPbrMaterialUserRegionEntries,
21
16
  createPbrSkinMeshBindGroupEntries,
@@ -26,18 +21,23 @@ import {
26
21
  import { COOKIE_MATRIX_BYTES } from '../prepare/extended-lighting/resources';
27
22
  import type { DispatchEntry, ExtractedLights } from '../render-system-extract';
28
23
  import { matchPass } from '../systems/pass-selector';
29
- import { worldEntityKey } from './frame-snapshot';
30
- import { resolveGeometryInstancesBindGroup } from './main-pass-geometry';
24
+ import {
25
+ resolveGeometryInstanceBuffer,
26
+ resolveGeometryInstancesBindGroup,
27
+ } from './main-pass-geometry';
28
+ import {
29
+ buildPerSubmeshMaterialBg,
30
+ type PerSubmeshMaterialBgDeps,
31
+ prepareMaterialSkylight,
32
+ } from './main-pass-material';
31
33
  import {
32
34
  getOrCreateFromChain,
33
- MAX_UNIFORM_INSTANCES,
34
35
  MESH_PER_ENTITY_STRIDE,
35
36
  MESH_SSBO_BYTES,
36
37
  MESH_UBO_FULL_ARRAY_BYTES,
37
- packInstanceStorageBuffer,
38
38
  } from './mesh-ssbo';
39
39
  import type { _InternalRenderPipelineContext } from './render-context';
40
- import { STANDARD_PBR_UBO_SIZE } from './render-context';
40
+ import { MATERIAL_PER_ENTITY_STRIDE, STANDARD_PBR_UBO_SIZE } from './render-context';
41
41
  import { POINTS_LINES_VIEW_BYTES, pointShadowViewOffset, VIEW_UNIFORM_BYTES } from './view-ubo';
42
42
 
43
43
  export const SHADOW_CASTER_SLOT_STRIDE = 256;
@@ -253,13 +253,15 @@ function ensureTypedShadowViewBg(
253
253
  }
254
254
 
255
255
  interface ShadowDispatch {
256
+ readonly vertexEntry: string | undefined;
257
+ readonly fragmentEntry: string | undefined;
256
258
  readonly materialShaderId: string;
257
259
  readonly renderState: MaterialRenderState | undefined;
258
260
  }
259
261
 
260
262
  type ShadowDispatchMap = ReadonlyMap<number, ReadonlyMap<number, ShadowDispatch>>;
261
263
 
262
- function shadowShaderMap(c: _InternalRenderPipelineContext): ShadowDispatchMap {
264
+ export function shadowShaderMap(c: _InternalRenderPipelineContext): ShadowDispatchMap {
263
265
  const shaders = new Map<number, Map<number, ShadowDispatch>>();
264
266
  for (const entry of c.dispatch) {
265
267
  if (entry.tags.LightMode === 'ShadowCaster' && entry.materialShaderId !== undefined) {
@@ -269,7 +271,12 @@ function shadowShaderMap(c: _InternalRenderPipelineContext): ShadowDispatchMap {
269
271
  shaders.set(entry.renderableIndex, byMaterial);
270
272
  }
271
273
  byMaterial.set(entry.materialHandle, {
272
- materialShaderId: entry.materialShaderId,
274
+ vertexEntry: entry.vertexEntry,
275
+ fragmentEntry: entry.fragmentEntry,
276
+ materialShaderId:
277
+ entry.materialShaderId === 'forgeax::default-standard-pbr'
278
+ ? SHADOW_CASTER_SHADER_ID
279
+ : entry.materialShaderId,
273
280
  renderState: entry.renderState,
274
281
  });
275
282
  }
@@ -371,13 +378,15 @@ export function encodeDirectionalShadowPass(
371
378
  `view-shadow-directional-${cascadeIndex}`,
372
379
  );
373
380
  const materialBg = ensureSpotShadowMaterialBg(c);
374
- if (pipeline === null || viewBg === null || materialBg === null) return;
375
- c.frameState.directionalShadowCacheRecorded = true;
381
+ if (pipeline === null || viewBg === null || materialBg === null) {
382
+ c.frameState.directionalShadowCacheRecorded = false;
383
+ return;
384
+ }
376
385
  pass.setViewport(viewport.x, viewport.y, viewport.w, viewport.h, 0, 1);
377
386
  pass.setPipeline(pipeline);
378
387
  pass.setBindGroup(0, viewBg, [0, 0]);
379
388
  pass.setBindGroup(1, materialBg, [0]);
380
- recordShadowCasterDraws(
389
+ const complete = recordShadowCasterDraws(
381
390
  c,
382
391
  pass,
383
392
  pipeline,
@@ -385,6 +394,8 @@ export function encodeDirectionalShadowPass(
385
394
  buildMatchedRenderableIndices(c.dispatch, { LightMode: ['ShadowCaster'] }),
386
395
  shadowShaderMap(c),
387
396
  );
397
+ c.frameState.directionalShadowCacheRecorded =
398
+ cascadeIndex === 0 ? complete : c.frameState.directionalShadowCacheRecorded && complete;
388
399
  }
389
400
 
390
401
  export function encodePointShadowPass(
@@ -569,7 +580,7 @@ function recordShadowCasterDraws(
569
580
  shadowMeshBindGroup: BindGroup,
570
581
  matchedIndices: Set<number> | null,
571
582
  shadowDispatchByRenderableIdx: ShadowDispatchMap,
572
- ): void {
583
+ ): boolean {
573
584
  const { runtime, pipelineState, validatedOrdered } = c;
574
585
  // M-3 / w12: vertexBuffer/indexBuffer state locals migrate to GpuBuffer
575
586
  // (the wrapper) -- the de-dup compare uses wrapper identity (one wrapper
@@ -583,6 +594,8 @@ function recordShadowCasterDraws(
583
594
  // bound by the setPipeline call above; the loop switches to a custom
584
595
  // ShadowCaster PSO when a material supplies one.
585
596
  let shadowLastPipeline: RenderPipeline = shadowPipeline;
597
+ let materialDeps: PerSubmeshMaterialBgDeps | undefined;
598
+ let complete = true;
586
599
 
587
600
  for (let i = 0; i < validatedOrdered.length; i++) {
588
601
  const entry = validatedOrdered[i];
@@ -593,7 +606,10 @@ function recordShadowCasterDraws(
593
606
 
594
607
  if (entry.source.skin !== undefined) {
595
608
  const skinPipeline = skinnedShadowPipeline(c, entry);
596
- if (skinPipeline === null) continue;
609
+ if (skinPipeline === null) {
610
+ complete = false;
611
+ continue;
612
+ }
597
613
  if (skinPipeline !== shadowLastPipeline) {
598
614
  shadowPass.setPipeline(skinPipeline);
599
615
  shadowLastPipeline = skinPipeline;
@@ -640,89 +656,25 @@ function recordShadowCasterDraws(
640
656
  shadowPass.setBindGroup(2, shadowMeshBindGroup, [i * MESH_PER_ENTITY_STRIDE]);
641
657
  }
642
658
 
643
- // C1 + C2 (w12): per-entity instance buffer + instanceCount
644
- let shadowInstanceBuffer: Buffer = pipelineState.identityInstanceBuffer;
645
- let shadowInstanceCount = 1;
646
- const shadowInst = entry.source.instances;
647
- if (shadowInst?.instanceCount === 0) continue;
648
- if (shadowInst !== undefined) {
649
- const uniformFallback = runtime.device.caps.storageBuffer === false;
650
- // Over-cap uniform fallback can't fit the per-instance window — bind
651
- // identity and let the shader collapse (same semantics as the main
652
- // pass). Otherwise build/upload the per-entity instance buffer: storage
653
- // by default, uniform when the device lacks storage buffers.
654
- if (uniformFallback && shadowInst.instanceCount > MAX_UNIFORM_INSTANCES) {
655
- shadowInstanceCount = shadowInst.instanceCount;
656
- shadowInstanceBuffer = pipelineState.identityInstanceBuffer;
657
- } else {
658
- const bufUsage = uniformFallback
659
- ? GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST
660
- : GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST;
661
- const instancePayload = uniformFallback
662
- ? shadowInst.transforms
663
- : packInstanceStorageBuffer(shadowInst.transforms);
664
- const requestedBytes = instancePayload.byteLength;
665
- const cached = c.frameState.instanceBuffers.get(
666
- worldEntityKey(entry.source.worldId, shadowInst.cacheKey),
667
- );
668
- let active: InstanceBufferCacheEntry | null = null;
669
- if (
670
- cached !== undefined &&
671
- cached.uploadedArchVersion === shadowInst.archVersion &&
672
- cached.uploadedByteLength === requestedBytes
673
- ) {
674
- active = cached;
675
- } else if (requestedBytes > 0) {
676
- const bufRes = runtime.device.createBuffer({
677
- size: requestedBytes,
678
- usage: bufUsage,
679
- mappedAtCreation: false,
680
- });
681
- if (!bufRes.ok) {
682
- runtime.errorRegistry.fire(bufRes.error);
683
- } else {
684
- // feat-20260619 M4 / F12: destroy the old cached buffer
685
- // before replacing it with the new one (D-6).
686
- if (cached !== undefined && !cached.buffer.isDestroyed) {
687
- const r = cached.buffer.destroy();
688
- if (!r.ok) runtime.errorRegistry.fire(r.error);
689
- }
690
- const newBuffer = new GpuBuffer(runtime.device, bufRes.value);
691
- active = {
692
- buffer: newBuffer,
693
- uploadedArchVersion: shadowInst.archVersion,
694
- uploadedByteLength: requestedBytes,
695
- };
696
- c.frameState.instanceBuffers.set(
697
- worldEntityKey(entry.source.worldId, shadowInst.cacheKey),
698
- active,
699
- );
700
- }
701
- }
702
- if (active !== null) {
703
- const writeRes = runtime.device.queue.writeBuffer(
704
- active.buffer.handle,
705
- 0,
706
- instancePayload,
707
- );
708
- if (!writeRes.ok) {
709
- runtime.errorRegistry.fire(writeRes.error);
710
- } else {
711
- shadowInstanceBuffer = active.buffer.handle;
712
- shadowInstanceCount = Math.max(1, shadowInst.instanceCount);
713
- }
714
- }
715
- }
716
- }
659
+ // Main and shadow passes share one instance residency owner and upload receipt.
660
+ const shadowInstanceDraws = resolveGeometryInstanceBuffer(
661
+ c,
662
+ entry,
663
+ [
664
+ {
665
+ instanceBuffer: pipelineState.identityInstanceBuffer,
666
+ instanceBindGroup: resolveGeometryInstancesBindGroup(
667
+ c,
668
+ pipelineState.identityInstanceBuffer,
669
+ ),
670
+ instanceCount: 1,
671
+ probeOffset: -1,
672
+ },
673
+ ],
674
+ false,
675
+ );
676
+ if (shadowInstanceDraws === null) continue;
717
677
 
718
- // Bind per-entity instances BG for @group(3) (or fallback identity).
719
- // D-4: write end of the HDRP shadow-instances producer/consumer pair.
720
- // outerKey = worldEntityKey(entry.source.worldId, entry.source.entityKey),
721
- // handle = shadowInstanceBuffer; the HDRP main pass read end (:3820 below)
722
- // must look up the same (compositeKey, instBuffer) leaf or the shadow
723
- // instances silently drop.
724
- const shadowInstancesBg = resolveGeometryInstancesBindGroup(c, shadowInstanceBuffer);
725
- shadowPass.setBindGroup(3, shadowInstancesBg);
726
678
  // feat-20260608 M4 / w16: per-submesh shadow draw loop.
727
679
  // Only draw submeshes whose topology is triangle-list or triangle-strip
728
680
  // (line-list / point-list submeshes cast no shadow and are skipped).
@@ -742,21 +694,17 @@ function recordShadowCasterDraws(
742
694
  ?.get(submeshMaterial.materialHandle ?? 0);
743
695
  const entryShadowShaderId = shadowDispatch?.materialShaderId;
744
696
  const entryShadowRenderState = shadowDispatch?.renderState;
745
- // A skinned caster's pipeline was selected above together with its
746
- // two-binding mesh/palette group. Keep that pairing for every submesh;
747
- // resetting to the static shadow pipeline here would bind the skin
748
- // group against `pbr-pl` and Dawn rejects the draw at submit time.
749
- let entryShadowPipeline =
697
+ // Canonical skin casters retain their palette layout. Authored casters
698
+ // select their own program below with the same geometry layout.
699
+ let entryShadowPipeline: RenderPipeline | null =
750
700
  entry.source.skin !== undefined ? shadowLastPipeline : shadowPipeline;
751
701
  if (
752
- entry.source.skin === undefined &&
753
702
  entryShadowShaderId !== undefined &&
754
703
  (entryShadowShaderId !== 'forgeax::default-shadow-caster' ||
755
- entryShadowRenderState !== undefined)
704
+ (entryShadowRenderState !== undefined && entry.source.skin === undefined))
756
705
  ) {
757
- // On a cache miss (async build in flight / build failure), fall back
758
- // to the default PSO so the caster still writes depth rather than
759
- // dropping its draw.
706
+ // Pending or failed authored programs must not draw using a different
707
+ // caster. Retry preparation on the next frame with the same program.
760
708
  entryShadowPipeline =
761
709
  runtime.getMaterialShaderPipeline?.(
762
710
  entryShadowShaderId,
@@ -766,19 +714,76 @@ function recordShadowCasterDraws(
766
714
  undefined, // indexFormat
767
715
  undefined, // variantSet — shadow caster has no variant axes
768
716
  'shadow-caster', // passKind
769
- ) ?? shadowPipeline;
717
+ undefined, // meshAttributes
718
+ 1,
719
+ undefined, // colorFormatOverride
720
+ undefined, // shaderUvSetCount
721
+ undefined, // depthFormatOverride
722
+ undefined, // vertexLayout
723
+ entry.mesh.layoutProjection,
724
+ undefined, // shaderModuleMode
725
+ entry.source.skin === undefined ? 'pbr' : 'pbr-skin',
726
+ undefined, // additionalColorFormats
727
+ shadowDispatch?.vertexEntry,
728
+ shadowDispatch?.fragmentEntry,
729
+ ) ?? null;
730
+ }
731
+ if (entryShadowPipeline === null) {
732
+ complete = false;
733
+ continue;
770
734
  }
771
735
  if (entryShadowPipeline !== shadowLastPipeline && entryShadowPipeline !== null) {
772
736
  shadowPass.setPipeline(entryShadowPipeline);
773
737
  shadowLastPipeline = entryShadowPipeline;
774
738
  }
775
- if (entry.mesh.indexed) {
776
- shadowPass.drawIndexed(sm.indexCount, shadowInstanceCount, sm.indexOffset, 0, 0);
739
+ if (entryShadowShaderId !== undefined && entryShadowShaderId !== SHADOW_CASTER_SHADER_ID) {
740
+ materialDeps ??= {
741
+ runtime,
742
+ pipelineState,
743
+ world: c.world,
744
+ store: c.store,
745
+ materialSlice: STANDARD_PBR_UBO_SIZE,
746
+ videoHighPerfAvailable: probeVideoHighPerfUpload(runtime.device),
747
+ skylightResources: prepareMaterialSkylight(c).skylightResources,
748
+ resolveRenderTargetTextureSource: runtime.resolveRenderTargetTextureSource,
749
+ materialBgShared: c.frameState.materialBgShared,
750
+ materialBgAssemblyCache: c.materialBgAssemblyCache,
751
+ frameState: c.frameState,
752
+ bindGroupCounts: c.bindGroupCounts,
753
+ };
754
+ const materialSlot =
755
+ c.materialSlotIndices[i]?.[sm.materialSlot] ?? c.materialSlotIndices[i]?.[0];
756
+ if (materialSlot === undefined) {
757
+ throw new RhiError({
758
+ code: 'rhi-descriptor-invalid',
759
+ expected: 'a prepared material slot for each shadow submesh',
760
+ hint: 'repair material slot preparation before recording shadows',
761
+ });
762
+ }
763
+ const materialGroup = buildPerSubmeshMaterialBg(
764
+ materialDeps,
765
+ submeshMaterial,
766
+ entry.source.entityKey,
767
+ entry.world ?? c.world,
768
+ entryShadowShaderId,
769
+ );
770
+ shadowPass.setBindGroup(1, materialGroup, [materialSlot * MATERIAL_PER_ENTITY_STRIDE]);
777
771
  } else {
778
- shadowPass.draw(sm.vertexCount, shadowInstanceCount, 0, 0);
772
+ const materialGroup = ensureSpotShadowMaterialBg(c);
773
+ if (materialGroup === null) continue;
774
+ shadowPass.setBindGroup(1, materialGroup, [0]);
775
+ }
776
+ for (const instanceDraw of shadowInstanceDraws) {
777
+ shadowPass.setBindGroup(3, instanceDraw.instanceBindGroup);
778
+ if (entry.mesh.indexed) {
779
+ shadowPass.drawIndexed(sm.indexCount, instanceDraw.instanceCount, sm.indexOffset, 0, 0);
780
+ } else {
781
+ shadowPass.draw(sm.vertexCount, instanceDraw.instanceCount, 0, 0);
782
+ }
779
783
  }
780
784
  }
781
785
  }
786
+ return complete;
782
787
  }
783
788
 
784
789
  /**
@@ -28,6 +28,7 @@ import {
28
28
  postProcessShaderEntrySignature,
29
29
  } from '../fullscreen-post-process-pass';
30
30
  import { GpuDrivenProduction } from '../gpu-driven/production-raster';
31
+ import { disposeInstanceBufferChunks, disposeInstanceBuffers } from '../instance-buffer-cache';
31
32
  import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
32
33
  import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
33
34
  import { inspectPointShadow } from '../point-shadow-inspection';
@@ -35,6 +36,7 @@ import type { PreparedGraphicsResolver } from '../prepare/prepared-graphics-reso
35
36
  import { validateRenderables } from '../record/frame';
36
37
  import { prepareFrameLighting } from '../record/frame-lighting';
37
38
  import type { RenderFrameState, ValidatedRenderable } from '../record/frame-snapshot';
39
+ import { resolveGeometryInstanceBuffer } from '../record/main-pass-geometry';
38
40
  import { computeSplitLdrSprite } from '../record/main-pass-sprite-draws';
39
41
  import {
40
42
  prepareRecoveryPipelineReadiness,
@@ -121,7 +123,7 @@ export interface RenderSystemRecoveryOwner {
121
123
  setLastSuccessfulFrameSeed(seed: RecoveryFrameSeed): void;
122
124
  prepareRecoveryGraphCandidate(
123
125
  runtime: RecoveryGraphCandidateRuntime,
124
- ): RecoveryGraphCandidatePreparation;
126
+ ): Promise<RecoveryGraphCandidatePreparation>;
125
127
  submitCandidateSetup(
126
128
  candidate: RecoveryGraphCandidate,
127
129
  isValid: () => boolean,
@@ -225,6 +227,8 @@ export function createRenderSystemRecovery(
225
227
  currentDirectionalShadowView: null,
226
228
  currentSpotShadowView: null,
227
229
  instanceBuffers: new Map(),
230
+ instanceBufferChunks: new Map(),
231
+ instanceResidency: new Map(),
228
232
  morphBuffers: new Map(),
229
233
  hdrpClusterBinScratch: createClusterBinScratch(),
230
234
  hdrpClusterGridScratch: null,
@@ -313,9 +317,9 @@ export function createRenderSystemRecovery(
313
317
  candidate.release();
314
318
  };
315
319
 
316
- const prepareRecoveryGraphCandidate = (
320
+ const prepareRecoveryGraphCandidate = async (
317
321
  runtime: RecoveryGraphCandidateRuntime,
318
- ): RecoveryGraphCandidatePreparation => {
322
+ ): Promise<RecoveryGraphCandidatePreparation> => {
319
323
  const seed = lastSuccessfulFrameSeed;
320
324
  if (seed === undefined) return { kind: 'no-seed' };
321
325
  const camera = seed.frame.cameras[0];
@@ -382,6 +386,13 @@ export function createRenderSystemRecovery(
382
386
  candidateGpuDrivenScene = undefined;
383
387
  candidatePointsLines?.release();
384
388
  candidatePointsLines = undefined;
389
+ disposeInstanceBuffers(candidateFrameState.instanceBuffers, runtime.internals.errorRegistry);
390
+ if (candidateFrameState.instanceBufferChunks !== undefined) {
391
+ disposeInstanceBufferChunks(
392
+ candidateFrameState.instanceBufferChunks,
393
+ runtime.internals.errorRegistry,
394
+ );
395
+ }
385
396
  candidateGpuStore.destroyAll();
386
397
  };
387
398
  const markPublished = (): void => {
@@ -403,10 +414,17 @@ export function createRenderSystemRecovery(
403
414
  if (!gpuScene.ok) return failed('candidate GPU scene preparation failed', gpuScene.error);
404
415
  candidateGpuDrivenScene = gpuScene.value;
405
416
  if (candidateGpuDrivenScene.state !== undefined) {
406
- candidateGpuDrivenProduction = GpuDrivenProduction.forDevice(
417
+ const production = await GpuDrivenProduction.forRecoveryDevice(
407
418
  runtime.internals.device,
408
419
  gpuDrivenShaderFactory,
420
+ (descriptor) =>
421
+ internals.createShaderModule === undefined
422
+ ? Promise.resolve(gpuDrivenShaderFactory.createShaderModule(descriptor))
423
+ : internals.createShaderModule(runtime.internals.device, descriptor),
409
424
  );
425
+ if (!production.ok)
426
+ return failed('candidate GPU-driven shader preparation failed', production.error);
427
+ candidateGpuDrivenProduction = production.value;
410
428
  const prepared = candidateGpuDrivenProduction.prepare({
411
429
  scene: candidateGpuDrivenScene.state,
412
430
  camera,
@@ -578,6 +596,25 @@ export function createRenderSystemRecovery(
578
596
  if (candidateValidated.length !== seed.frame.renderables.length) {
579
597
  return failed('candidate validation did not retain every LKG-visible renderable');
580
598
  }
599
+ for (const entry of candidateValidated) {
600
+ if (entry.source.instances === undefined) continue;
601
+ const prepared = resolveGeometryInstanceBuffer(
602
+ {
603
+ runtime: runtime.internals,
604
+ pipelineState: runtime.pipelineState,
605
+ frameState: candidateFrameState,
606
+ bindGroupCounts: { createBindGroup: 0, keys: [] },
607
+ },
608
+ entry,
609
+ [],
610
+ false,
611
+ );
612
+ if (prepared === null) return failed('candidate instance preparation failed');
613
+ }
614
+ // These uploads belong to recovery preparation, never to a rendered frame.
615
+ for (const [id, residency] of candidateFrameState.instanceResidency ?? []) {
616
+ candidateFrameState.instanceResidency?.set(id, { ...residency, frameNumber: -1 });
617
+ }
581
618
  const lighting = prepareFrameLighting(
582
619
  runtime.internals,
583
620
  candidateFrameState,
@@ -602,7 +639,11 @@ export function createRenderSystemRecovery(
602
639
  seed.frame.lights.pointShadow.length > 0 ||
603
640
  seed.frame.lights.spot.some((spot) => spot.castShadow === true) ||
604
641
  (seed.frame.lights.lightViewProj?.length ?? 0) > 0,
605
- splitLdrSprite: computeSplitLdrSprite(candidateValidated, camera.tonemap !== 'none'),
642
+ splitLdrSprite: computeSplitLdrSprite(
643
+ candidateValidated,
644
+ camera.tonemap !== 'none',
645
+ seed.frame.dispatch,
646
+ ),
606
647
  reflectionFallbackAvailable: candidateFrameState.reflectionFallbackDemand === true,
607
648
  });
608
649
  const candidatePostProcessIds = new Set<string>([
@@ -42,6 +42,7 @@ import type {
42
42
  TemporalTargetInspection,
43
43
  TransmissionInspection,
44
44
  } from './inspection-types';
45
+ import type { InstanceCollectionInspection } from './instances';
45
46
  import type { SsrAdmissionIdentity } from './ssr/identity';
46
47
  import type {
47
48
  RenderTarget,
@@ -294,6 +295,7 @@ export interface SpotLightProjectorFrameContext {
294
295
  * leases, receipts, inspection PODs, and recovery Result values.
295
296
  */
296
297
  export interface Renderer {
298
+ /** Renderer-owned authoring and update path for large instance collections. */
297
299
  attach(world: World): RenderResult<RenderWorldLease, RenderError>;
298
300
  draw(input: RenderFrameInput): RenderResult<FrameReceipt, RenderError>;
299
301
  createRenderTarget(descriptor: RenderTargetDescriptor): RenderResult<RenderTarget, RenderError>;
@@ -509,6 +511,8 @@ export interface RenderInspection {
509
511
  readonly featureDiagnostics: readonly RenderFeatureDiagnostics[];
510
512
  readonly frustumStats: { readonly culled: number; readonly total: number };
511
513
  readonly visibilityStats: { readonly explicitlyHidden: number };
514
+ /** Renderer-owned large-instance residency and upload facts. */
515
+ readonly instanceCollections: readonly InstanceCollectionInspection[];
512
516
  readonly renderScene: RenderSceneInspection;
513
517
  readonly reflectionProbes: ReflectionProbeInspection;
514
518
  /** Renderer-owned SSR dependency seam; consumer facts are projected from live owners. */