@forgeax/engine-render 0.1.35 → 0.1.36

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 (87) hide show
  1. package/README.md +17 -0
  2. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  3. package/dist/{chunk-MXTWKZO4.mjs → chunk-2OZAS3WU.mjs} +225 -4
  4. package/dist/chunk-2OZAS3WU.mjs.map +1 -0
  5. package/dist/{chunk-G6HDNGY7.mjs → chunk-BZQ4CDRF.mjs} +24 -230
  6. package/dist/chunk-BZQ4CDRF.mjs.map +1 -0
  7. package/dist/{chunk-LNIUIKQ7.mjs → chunk-CJOV2XLO.mjs} +77 -19
  8. package/dist/chunk-CJOV2XLO.mjs.map +1 -0
  9. package/dist/{chunk-3XUBWRY2.mjs → chunk-D6WSBXJ2.mjs} +4 -4
  10. package/dist/{chunk-3XUBWRY2.mjs.map → chunk-D6WSBXJ2.mjs.map} +1 -1
  11. package/dist/{chunk-LM5IE7GF.mjs → chunk-DRF6OULX.mjs} +3 -3
  12. package/dist/{chunk-LM5IE7GF.mjs.map → chunk-DRF6OULX.mjs.map} +1 -1
  13. package/dist/{chunk-SGPUJW3F.mjs → chunk-OB5WAFYH.mjs} +3 -3
  14. package/dist/{chunk-SGPUJW3F.mjs.map → chunk-OB5WAFYH.mjs.map} +1 -1
  15. package/dist/{chunk-MZ4C7EHF.mjs → chunk-OEYKKMXG.mjs} +5 -5
  16. package/dist/{chunk-MZ4C7EHF.mjs.map → chunk-OEYKKMXG.mjs.map} +1 -1
  17. package/dist/construct-renderer.mjs +130 -71
  18. package/dist/construct-renderer.mjs.map +1 -1
  19. package/dist/device/device-scope.d.ts.map +1 -1
  20. package/dist/features/particle-mesh-layout.d.ts +19 -0
  21. package/dist/features/particle-mesh-layout.d.ts.map +1 -0
  22. package/dist/features/types.d.ts +5 -1
  23. package/dist/features/types.d.ts.map +1 -1
  24. package/dist/gpu-driven/production-raster.d.ts +1 -1
  25. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  26. package/dist/gpu-driven/shadow-views.d.ts +1 -1
  27. package/dist/gpu-driven/shadow-views.d.ts.map +1 -1
  28. package/dist/gpu-driven/view-gpu.d.ts +1 -1
  29. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  30. package/dist/index.mjs +48 -36
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/internal.mjs +5 -5
  33. package/dist/pbr-pipeline.d.ts.map +1 -1
  34. package/dist/publication/publisher.d.ts +1 -0
  35. package/dist/publication/publisher.d.ts.map +1 -1
  36. package/dist/record/frame.d.ts.map +1 -1
  37. package/dist/record/prepared-material-bindings.d.ts +1 -2
  38. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  39. package/dist/record/render-context.d.ts +2 -0
  40. package/dist/record/render-context.d.ts.map +1 -1
  41. package/dist/record/shadow-pass.d.ts +2 -2
  42. package/dist/record/shadow-pass.d.ts.map +1 -1
  43. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  44. package/dist/recovery/render-system-candidate.d.ts +1 -8
  45. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  46. package/dist/render-system-extract-tail.d.ts.map +1 -1
  47. package/dist/render-system-extract.d.ts.map +1 -1
  48. package/dist/render-system.d.ts.map +1 -1
  49. package/dist/temporal/index.mjs +4 -4
  50. package/dist/typed-shadow-passes.d.ts.map +1 -1
  51. package/package.json +21 -21
  52. package/src/__tests__/device-scope-lifecycle.unit.test.ts +26 -0
  53. package/src/__tests__/gpu-driven-production.integration.test.ts +18 -1
  54. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +23 -0
  55. package/src/__tests__/material-vertex-texture.browser.test.ts +7 -0
  56. package/src/__tests__/material-vertex-texture.dawn.test.ts +7 -0
  57. package/src/__tests__/material-vertex-texture.fixture.ts +31 -0
  58. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +5 -0
  59. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +106 -4
  60. package/src/__tests__/runtime-material-program.integration.test.ts +79 -0
  61. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +23 -3
  62. package/src/assembly/webgpu-renderer.ts +12 -9
  63. package/src/assembly/webgpu-vertex-layouts.ts +2 -9
  64. package/src/device/device-scope.ts +3 -0
  65. package/src/features/particle-mesh-layout.ts +25 -0
  66. package/src/features/types.ts +5 -1
  67. package/src/gpu-driven/production-raster.ts +9 -3
  68. package/src/gpu-driven/shadow-views.ts +27 -2
  69. package/src/gpu-driven/view-gpu.ts +5 -0
  70. package/src/pbr-pipeline.ts +4 -1
  71. package/src/publication/__tests__/features.integration.test.ts +51 -0
  72. package/src/publication/__tests__/publication.integration.test.ts +45 -0
  73. package/src/publication/publisher.ts +56 -29
  74. package/src/record/__tests__/prepared-runtime-material.integration.test.ts +134 -0
  75. package/src/record/frame.ts +45 -6
  76. package/src/record/prepared-material-bindings.ts +11 -37
  77. package/src/record/render-context.ts +2 -0
  78. package/src/record/shadow-pass.ts +65 -4
  79. package/src/record/typed-frame-graph.ts +5 -1
  80. package/src/recovery/render-system-candidate.ts +1 -9
  81. package/src/render-system-extract-tail.ts +15 -6
  82. package/src/render-system-extract.ts +9 -2
  83. package/src/render-system.ts +0 -1
  84. package/src/typed-shadow-passes.ts +36 -5
  85. package/dist/chunk-G6HDNGY7.mjs.map +0 -1
  86. package/dist/chunk-LNIUIKQ7.mjs.map +0 -1
  87. package/dist/chunk-MXTWKZO4.mjs.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { probeVideoHighPerfUpload } from '@forgeax/engine-graphics-extras';
2
+ import { box3, frustum } from '@forgeax/engine-math';
2
3
  import {
3
4
  type BindGroup,
4
5
  type BindGroupEntry,
@@ -24,6 +25,7 @@ import {
24
25
  import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
25
26
  import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
26
27
  import { instanceUploadRangesForResident } from '../instances';
28
+ import { deriveInstancesUnionBounds } from '../instances-derived-bounds';
27
29
  import {
28
30
  buildPbrMaterialUserRegionEntries,
29
31
  createPbrSkinMeshBindGroupEntries,
@@ -643,6 +645,29 @@ function shadowCasterKeys(c: _InternalRenderPipelineContext): ReadonlySet<string
643
645
  return c.gpuDrivenShadowDrawKeys ?? new Set<string>();
644
646
  }
645
647
 
648
+ function shadowViewContains(source: RenderableSnapshot, planes: Float32Array | undefined): boolean {
649
+ if (planes === undefined || planes.length === 0 || source.localAabb === undefined) return true;
650
+ // The extracted mesh AABB is bind-pose geometry, not a conservative bound
651
+ // for the current skin palette. Keep skinned residuals until extraction owns
652
+ // a deformation-aware bound.
653
+ if (source.skin !== undefined) return true;
654
+ if (source.spriteInstances !== undefined) return true;
655
+ let worldBounds: Float32Array;
656
+ if (source.instances !== undefined) {
657
+ if (source.instances.instanceCount === 0) return false;
658
+ const derived = deriveInstancesUnionBounds({
659
+ meshAabb: source.localAabb,
660
+ entityWorld: source.transform.world,
661
+ transforms: source.instances.transforms,
662
+ });
663
+ if (derived === undefined) return true;
664
+ worldBounds = derived;
665
+ } else {
666
+ worldBounds = box3.transformBox3(box3.create(), source.localAabb, source.transform.world);
667
+ }
668
+ return frustum.intersectsBox(planes as frustum.Frustum, worldBounds as box3.Box3Like);
669
+ }
670
+
646
671
  function validateShadowOwnership(
647
672
  c: _InternalRenderPipelineContext,
648
673
  identity: ShadowViewIdentity,
@@ -654,12 +679,39 @@ function validateShadowOwnership(
654
679
  (c.gpuDrivenShadowDrawKeys === undefined || c.gpuDrivenShadowDrawKeys.size === 0)
655
680
  )
656
681
  return;
657
- const expectedKeys = shadowCasterKeys(c);
682
+ const viewPlanes = c.shadowViewPlanesByView?.get(shadowViewIdentityKey(identity));
683
+ const visibleRenderableIndices =
684
+ viewPlanes === undefined
685
+ ? undefined
686
+ : new Set(
687
+ (c.shadowValidatedOrdered ?? c.validatedOrdered)
688
+ .filter((entry) => shadowViewContains(entry.source, viewPlanes))
689
+ .map((entry) => entry.renderableIndex),
690
+ );
691
+ const allExpectedKeys = shadowCasterKeys(c);
692
+ const visibleExpectedKeys =
693
+ visibleRenderableIndices === undefined || c.shadowCasterMembership === undefined
694
+ ? allExpectedKeys
695
+ : new Set(
696
+ c.shadowCasterMembership
697
+ .filter((membership) => visibleRenderableIndices.has(membership.renderableIndex))
698
+ .map((membership) =>
699
+ gpuDrivenShadowDrawKey(
700
+ membership.worldEntity,
701
+ membership.materialHandle,
702
+ membership.drawItemIndex,
703
+ membership.passIndex,
704
+ ),
705
+ ),
706
+ );
658
707
  const claimed = claimedKeys ?? new Set<string>();
659
708
  const overlap = [...claimed].filter((key) => recordedKeys.has(key));
660
709
  const union = new Set([...claimed, ...recordedKeys]);
661
- const missing = [...expectedKeys].filter((key) => !union.has(key));
662
- const unexpectedClaims = [...claimed].filter((key) => !expectedKeys.has(key));
710
+ const missing = [...visibleExpectedKeys].filter((key) => !union.has(key));
711
+ // GPU ownership is projected before its own per-view culling. A claim may
712
+ // therefore be outside the CPU residual's conservative visible subset and
713
+ // is still valid as long as it belongs to the extracted caster membership.
714
+ const unexpectedClaims = [...claimed].filter((key) => !allExpectedKeys.has(key));
663
715
  // `shadowCasterMembership` describes the concrete draws that the GPU
664
716
  // producer is eligible to claim. A renderable that stays on the CPU lane
665
717
  // (for example a clustered frame without a compatible GPU-driven PBR
@@ -674,7 +726,7 @@ function validateShadowOwnership(
674
726
  throw new RhiError({
675
727
  code: 'internal-error',
676
728
  expected: `disjoint ShadowCaster ownership for ${shadowViewIdentityKey(identity)}`,
677
- hint: `claimed=${[...claimed].sort().join(',')} recorded=${[...recordedKeys].sort().join(',')} missing=${missing.sort().join(',')} unexpectedClaims=${unexpectedClaims.sort().join(',')}`,
729
+ hint: `expected=${[...allExpectedKeys].sort().join(',')} visibleExpected=${[...visibleExpectedKeys].sort().join(',')} claimed=${[...claimed].sort().join(',')} recorded=${[...recordedKeys].sort().join(',')} missing=${missing.sort().join(',')} unexpectedClaims=${unexpectedClaims.sort().join(',')}`,
678
730
  });
679
731
  }
680
732
 
@@ -842,9 +894,11 @@ export function encodeDirectionalShadowPass(
842
894
  cascadeIndex: number,
843
895
  viewport: { readonly x: number; readonly y: number; readonly w: number; readonly h: number },
844
896
  drawFeatures?: (view: BindGroup) => void,
897
+ forceRaster = false,
845
898
  ): void {
846
899
  const directionalIdentity = { kind: 'directional' as const, index: cascadeIndex };
847
900
  if (
901
+ !forceRaster &&
848
902
  (c.gpuDrivenShadowViews?.cacheHit(directionalIdentity) === true ||
849
903
  (c.gpuDrivenShadowViews === undefined && c.directionalShadowCacheReuse)) &&
850
904
  drawFeatures === undefined
@@ -941,6 +995,8 @@ export function encodeDirectionalShadowPass(
941
995
  }),
942
996
  shadowShaderMap(c),
943
997
  claimedKeys,
998
+ false,
999
+ c.shadowViewPlanesByView?.get(shadowViewIdentityKey(directionalIdentity)),
944
1000
  );
945
1001
  validateShadowOwnership(c, directionalIdentity, claimedKeys, recordedKeys);
946
1002
  c.frameState.directionalShadowCacheRecorded =
@@ -1047,6 +1103,7 @@ export function encodePointShadowPass(
1047
1103
  shadowShaderMap(c),
1048
1104
  claimedKeys,
1049
1105
  true,
1106
+ c.shadowViewPlanesByView?.get(shadowViewIdentityKey(pointIdentity)),
1050
1107
  );
1051
1108
  validateShadowOwnership(c, pointIdentity, claimedKeys, recordedKeys);
1052
1109
  }
@@ -1171,6 +1228,8 @@ export function encodeSpotShadowPass(
1171
1228
  }),
1172
1229
  shadowShaderMap(c),
1173
1230
  claimedKeys,
1231
+ false,
1232
+ c.shadowViewPlanesByView?.get(shadowViewIdentityKey(spotIdentity)),
1174
1233
  );
1175
1234
  validateShadowOwnership(c, spotIdentity, claimedKeys, recordedKeys);
1176
1235
  }
@@ -1303,6 +1362,7 @@ export function recordShadowCasterDraws(
1303
1362
  shadowDispatchByRenderableIdx: ShadowDispatchMap,
1304
1363
  gpuDrivenShadowDrawKeys: ReadonlySet<string> | undefined,
1305
1364
  reflected = false,
1365
+ viewPlanes?: Float32Array,
1306
1366
  ): ReadonlySet<string> {
1307
1367
  const {
1308
1368
  runtime,
@@ -1330,6 +1390,7 @@ export function recordShadowCasterDraws(
1330
1390
  for (let i = 0; i < validatedOrdered.length; i++) {
1331
1391
  const entry = validatedOrdered[i];
1332
1392
  if (entry === undefined) continue;
1393
+ if (!shadowViewContains(entry.source, viewPlanes)) continue;
1333
1394
 
1334
1395
  // feat-20260609 M2: skip entities that don't match the pass selector.
1335
1396
  if (matchedIndices !== null && !matchedIndices.has(entry.renderableIndex)) continue;
@@ -2639,7 +2639,11 @@ export function ensureCompiledFrameGraph(
2639
2639
  ? {}
2640
2640
  : {
2641
2641
  projectGpuDrivenShadow: (identity) =>
2642
- gpuDriven.projectShadow?.(builder, identity) ?? ok(undefined),
2642
+ // A newly compiled graph must include the shadow compute passes
2643
+ // even when the producer cache hit. The graph owns its pass
2644
+ // list; importing only the cached view resources would reuse
2645
+ // stale indirect arguments after a topology replacement.
2646
+ gpuDriven.projectShadow?.(builder, identity, true) ?? ok(undefined),
2643
2647
  }),
2644
2648
  contributeShadowFeatures: () =>
2645
2649
  (featureShadows ??= projectRenderFeatureShadows(
@@ -53,6 +53,7 @@ import { prepareFrameLighting } from '../record/frame-lighting';
53
53
  import type { RenderFrameState, ValidatedRenderable } from '../record/frame-snapshot';
54
54
  import { resolveGeometryInstanceBuffer } from '../record/main-pass-geometry';
55
55
  import { computeSplitLdrSprite } from '../record/main-pass-sprite-draws';
56
+ import type { PreparedResolverCaches } from '../record/prepared-material-bindings';
56
57
  import { ensureProbeBlendRecordBuffer } from '../record/probe-blend-buffer';
57
58
  import {
58
59
  prepareRecoveryPipelineReadiness,
@@ -90,15 +91,6 @@ import type {
90
91
  RecoveryPointsLinesCandidate,
91
92
  } from './types';
92
93
 
93
- export interface PreparedResolverCaches {
94
- readonly preparedPipelineIds: WeakMap<object, string>;
95
- readonly preparedMaterialPipelineShaders: WeakMap<object, string>;
96
- readonly preparedGroup0Pipelines: WeakSet<object>;
97
- readonly preparedViewOnlyPipelines: WeakSet<object>;
98
- readonly preparedRenderMaterialPipelines: WeakSet<object>;
99
- readonly preparedAssetHandles: WeakMap<RenderResourceScope, Map<string, number>>;
100
- }
101
-
102
94
  export interface RecoveryFrameSeed {
103
95
  readonly frame: ExtractedFrame;
104
96
  readonly worlds: readonly RenderResourceScope[];
@@ -2137,6 +2137,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2137
2137
  if (asset.kind !== 'material') {
2138
2138
  materialSnap = defaultMaterialSnapshot(handleRaw);
2139
2139
  } else {
2140
+ // Program selection belongs to the shared source; RuntimeMaterialValue
2141
+ // only projects the numeric parameter content resolved below.
2142
+ const source = world.sharedRefs.resolve<string, Asset>(tagged);
2143
+ const programOwner =
2144
+ source.ok && source.value.kind === 'material' ? source.value : asset;
2140
2145
  // feat-20260529 M3 / w11: material parent chain inheritance via
2141
2146
  // read-through _materialWalk accessor (plan-strategy D-6).
2142
2147
  // The old direct asset.passes / asset.values read never
@@ -2186,7 +2191,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2186
2191
  };
2187
2192
  try {
2188
2193
  materialProgramSelections = materialProgramSelectionsForMaterial(
2189
- asset,
2194
+ programOwner,
2190
2195
  assets,
2191
2196
  materialContext,
2192
2197
  );
@@ -2198,10 +2203,14 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2198
2203
  // publication error instead of guessing from module names/order.
2199
2204
  if (isMissingMaterialProgram(error) && materialContext?.geometry === 'mesh') {
2200
2205
  try {
2201
- const skinnedSelections = materialProgramSelectionsForMaterial(asset, assets, {
2202
- ...materialContext,
2203
- geometry: 'skinned',
2204
- });
2206
+ const skinnedSelections = materialProgramSelectionsForMaterial(
2207
+ programOwner,
2208
+ assets,
2209
+ {
2210
+ ...materialContext,
2211
+ geometry: 'skinned',
2212
+ },
2213
+ );
2205
2214
  if (
2206
2215
  skinnedSelections?.some(materialProgramSelectionRequiresSkin) === true &&
2207
2216
  gpuDrivenMesh !== undefined
@@ -2238,7 +2247,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2238
2247
  ]),
2239
2248
  );
2240
2249
  const materialSceneIndexProgramKeys = materialSceneIndexProgramKeysForMaterial(
2241
- asset,
2250
+ programOwner,
2242
2251
  assets,
2243
2252
  materialContext,
2244
2253
  );
@@ -2362,9 +2362,16 @@ export function resolveMaterialSnapshot(
2362
2362
  if (!resolvedResult.ok) return defaultMaterialSnapshot(handleRaw);
2363
2363
  const resolved = resolvedResult.value;
2364
2364
  const allPasses = resolved.passes;
2365
- const materialProgramKeys = materialProgramKeysForMaterial(asset, assetsRef, materialContext);
2365
+ // Numeric World content changes values, never the immutable program owner.
2366
+ const source = world.sharedRefs.resolve<string, Asset>(tagged);
2367
+ const programOwner = source.ok && source.value.kind === 'material' ? source.value : asset;
2368
+ const materialProgramKeys = materialProgramKeysForMaterial(
2369
+ programOwner,
2370
+ assetsRef,
2371
+ materialContext,
2372
+ );
2366
2373
  const materialSceneIndexProgramKeys = materialSceneIndexProgramKeysForMaterial(
2367
- asset,
2374
+ programOwner,
2368
2375
  assetsRef,
2369
2376
  materialContext,
2370
2377
  );
@@ -1851,7 +1851,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1851
1851
  ) {
1852
1852
  return preparedMaterialBindings(
1853
1853
  runtime,
1854
- caches,
1855
1854
  worlds,
1856
1855
  materialShaderId,
1857
1856
  material.world,
@@ -109,6 +109,13 @@ export function addTypedShadowPasses(
109
109
  index: cascade,
110
110
  });
111
111
  if (shadowProjection !== undefined && !shadowProjection.ok) return shadowProjection;
112
+ // Every pass in this freshly compiled graph owns a newly allocated
113
+ // raster target. Populate it once even when the producer cache hits or
114
+ // the GPU shadow projection is not admitted on this frame. The encoder
115
+ // receives the same one-shot fence so its cache-hit fast path cannot
116
+ // return before setting the atlas viewport and recording the casters.
117
+ let forceRasterPending = true;
118
+ let forceRasterEncodePending = false;
112
119
  const added = recordShadowPass(
113
120
  graph,
114
121
  `shadowCascade${cascade}`,
@@ -128,10 +135,20 @@ export function addTypedShadowPasses(
128
135
  shadowFeatures === undefined
129
136
  ? undefined
130
137
  : (view) => shadowFeatures.encode(pass, frame, resources, view),
138
+ forceRasterEncodePending,
131
139
  ),
132
- (frame) =>
133
- shadowFeatures !== undefined ||
134
- !shadowViewCacheReuse(frame, { kind: 'directional', index: cascade }),
140
+ (frame) => {
141
+ forceRasterEncodePending = false;
142
+ if (forceRasterPending) {
143
+ forceRasterPending = false;
144
+ forceRasterEncodePending = true;
145
+ return true;
146
+ }
147
+ return (
148
+ shadowFeatures !== undefined ||
149
+ !shadowViewCacheReuse(frame, { kind: 'directional', index: cascade })
150
+ );
151
+ },
135
152
  [
136
153
  ...(shadowProjection?.ok === true && shadowProjection.value?.graphResources !== undefined
137
154
  ? shadowViewRasterAccesses(shadowProjection.value.graphResources)
@@ -197,6 +214,7 @@ export function addTypedShadowPasses(
197
214
  face,
198
215
  });
199
216
  if (shadowProjection !== undefined && !shadowProjection.ok) return shadowProjection;
217
+ let forceRasterPending = true;
200
218
  const identity = { kind: 'point' as const, index: pointIndex, face };
201
219
  const cacheReuse = (frame: RenderPipelineFrame): boolean =>
202
220
  shadowViewCacheReuse(frame, identity);
@@ -228,7 +246,13 @@ export function addTypedShadowPasses(
228
246
  ? undefined
229
247
  : (view) => shadowFeatures.encode(pass, frame, resources, view),
230
248
  ),
231
- (frame) => shadowFeatures !== undefined || !cacheReuse(frame),
249
+ (frame) => {
250
+ if (forceRasterPending) {
251
+ forceRasterPending = false;
252
+ return true;
253
+ }
254
+ return shadowFeatures !== undefined || !cacheReuse(frame);
255
+ },
232
256
  [
233
257
  ...(shadowProjection?.ok === true &&
234
258
  shadowProjection.value?.graphResources !== undefined
@@ -250,6 +274,7 @@ export function addTypedShadowPasses(
250
274
  for (let spotIndex = 0; spotIndex < Math.max(1, topology.shadow.spotCount); spotIndex += 1) {
251
275
  const shadowProjection = projectGpuDrivenShadow?.({ kind: 'spot', index: spotIndex });
252
276
  if (shadowProjection !== undefined && !shadowProjection.ok) return shadowProjection;
277
+ let forceRasterPending = true;
253
278
  const identity = { kind: 'spot' as const, index: spotIndex };
254
279
  const cacheReuse = (frame: RenderPipelineFrame): boolean =>
255
280
  shadowViewCacheReuse(frame, identity);
@@ -267,7 +292,13 @@ export function addTypedShadowPasses(
267
292
  ? undefined
268
293
  : (view) => shadowFeatures.encode(pass, frame, resources, view),
269
294
  ),
270
- (frame) => shadowFeatures !== undefined || !cacheReuse(frame),
295
+ (frame) => {
296
+ if (forceRasterPending) {
297
+ forceRasterPending = false;
298
+ return true;
299
+ }
300
+ return shadowFeatures !== undefined || !cacheReuse(frame);
301
+ },
271
302
  [
272
303
  ...(shadowProjection?.ok === true && shadowProjection.value?.graphResources !== undefined
273
304
  ? shadowViewRasterAccesses(shadowProjection.value.graphResources)