@forgeax/engine-render 0.1.34 → 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 (273) hide show
  1. package/README.md +69 -3
  2. package/dist/assembly/device-feature-admission.d.ts +2 -2
  3. package/dist/assembly/host-contract.d.ts +3 -2
  4. package/dist/assembly/host-contract.d.ts.map +1 -1
  5. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  6. package/dist/assembly/render-target-host.d.ts +5 -0
  7. package/dist/assembly/render-target-host.d.ts.map +1 -1
  8. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  9. package/dist/assembly/webgpu-renderer.d.ts +1 -0
  10. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  11. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  12. package/dist/authoring.mjs +2 -4
  13. package/dist/authoring.mjs.map +1 -1
  14. package/dist/chunk-2OZAS3WU.mjs +3489 -0
  15. package/dist/chunk-2OZAS3WU.mjs.map +1 -0
  16. package/dist/{chunk-UI5SFODX.mjs → chunk-BZQ4CDRF.mjs} +2338 -1991
  17. package/dist/chunk-BZQ4CDRF.mjs.map +1 -0
  18. package/dist/{chunk-MKDIT2EM.mjs → chunk-CJOV2XLO.mjs} +216 -261
  19. package/dist/chunk-CJOV2XLO.mjs.map +1 -0
  20. package/dist/chunk-D6WSBXJ2.mjs +1842 -0
  21. package/dist/chunk-D6WSBXJ2.mjs.map +1 -0
  22. package/dist/{chunk-HEB564WF.mjs → chunk-DRF6OULX.mjs} +4 -4
  23. package/dist/{chunk-HEB564WF.mjs.map → chunk-DRF6OULX.mjs.map} +1 -1
  24. package/dist/{chunk-OCDLEI6I.mjs → chunk-OB5WAFYH.mjs} +117 -5
  25. package/dist/chunk-OB5WAFYH.mjs.map +1 -0
  26. package/dist/{chunk-ODZWR6JQ.mjs → chunk-OEYKKMXG.mjs} +2042 -27
  27. package/dist/chunk-OEYKKMXG.mjs.map +1 -0
  28. package/dist/chunk-RIPAEGI6.mjs +48 -0
  29. package/dist/chunk-RIPAEGI6.mjs.map +1 -0
  30. package/dist/{chunk-ELJFJ2BU.mjs → chunk-X2KA6WHM.mjs} +29 -4
  31. package/dist/chunk-X2KA6WHM.mjs.map +1 -0
  32. package/dist/{chunk-BLXGONR2.mjs → chunk-XSFB7XXQ.mjs} +51 -4
  33. package/dist/chunk-XSFB7XXQ.mjs.map +1 -0
  34. package/dist/cloud/frame-context.d.ts +2 -2
  35. package/dist/cloud/frame-context.d.ts.map +1 -1
  36. package/dist/construct-renderer.mjs +936 -2140
  37. package/dist/construct-renderer.mjs.map +1 -1
  38. package/dist/device/device-scope.d.ts.map +1 -1
  39. package/dist/device/gpu-residency.d.ts +19 -19
  40. package/dist/device/gpu-residency.d.ts.map +1 -1
  41. package/dist/errors/render.d.ts +3 -2
  42. package/dist/errors/render.d.ts.map +1 -1
  43. package/dist/extract/material-context.d.ts +7 -0
  44. package/dist/extract/material-context.d.ts.map +1 -0
  45. package/dist/features/host.d.ts +5 -0
  46. package/dist/features/host.d.ts.map +1 -1
  47. package/dist/features/particle-mesh-layout.d.ts +19 -0
  48. package/dist/features/particle-mesh-layout.d.ts.map +1 -0
  49. package/dist/features/plan.d.ts +2 -0
  50. package/dist/features/plan.d.ts.map +1 -1
  51. package/dist/features/types.d.ts +8 -0
  52. package/dist/features/types.d.ts.map +1 -1
  53. package/dist/glyph-text-layout-system.d.ts +1 -1
  54. package/dist/glyph-text-layout-system.d.ts.map +1 -1
  55. package/dist/gpu-driven/production-raster.d.ts +1 -1
  56. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  57. package/dist/gpu-driven/shadow-ownership.d.ts +3 -3
  58. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -1
  59. package/dist/gpu-driven/shadow-views.d.ts +1 -1
  60. package/dist/gpu-driven/shadow-views.d.ts.map +1 -1
  61. package/dist/gpu-driven/view-gpu.d.ts +1 -1
  62. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  63. package/dist/ibl/frame-resources.d.ts.map +1 -1
  64. package/dist/ibl/skylight-bind-group.d.ts +4 -4
  65. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  66. package/dist/index.d.ts +3 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.mjs +542 -9
  69. package/dist/index.mjs.map +1 -1
  70. package/dist/instances.d.ts +3 -4
  71. package/dist/instances.d.ts.map +1 -1
  72. package/dist/internal.mjs +7 -8
  73. package/dist/internal.mjs.map +1 -1
  74. package/dist/pbr-pipeline.d.ts +3 -3
  75. package/dist/pbr-pipeline.d.ts.map +1 -1
  76. package/dist/pipeline/standard-output/lut-gpu.d.ts +2 -2
  77. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -1
  78. package/dist/pipeline/standard-output/resources.d.ts +2 -2
  79. package/dist/pipeline/standard-output/resources.d.ts.map +1 -1
  80. package/dist/publication/contract.d.ts +77 -0
  81. package/dist/publication/contract.d.ts.map +1 -0
  82. package/dist/publication/dependencies.d.ts +7 -0
  83. package/dist/publication/dependencies.d.ts.map +1 -0
  84. package/dist/publication/prepare-geometry.d.ts +9 -0
  85. package/dist/publication/prepare-geometry.d.ts.map +1 -0
  86. package/dist/publication/programs.d.ts +5 -0
  87. package/dist/publication/programs.d.ts.map +1 -0
  88. package/dist/publication/publisher.d.ts +34 -0
  89. package/dist/publication/publisher.d.ts.map +1 -0
  90. package/dist/publication/receiver.d.ts +29 -0
  91. package/dist/publication/receiver.d.ts.map +1 -0
  92. package/dist/publication/resource-scope.d.ts +18 -0
  93. package/dist/publication/resource-scope.d.ts.map +1 -0
  94. package/dist/publication/targets.d.ts +33 -0
  95. package/dist/publication/targets.d.ts.map +1 -0
  96. package/dist/publication/video.d.ts +5 -0
  97. package/dist/publication/video.d.ts.map +1 -0
  98. package/dist/record/fold-buckets.d.ts +3 -3
  99. package/dist/record/fold-buckets.d.ts.map +1 -1
  100. package/dist/record/frame-snapshot.d.ts +6 -6
  101. package/dist/record/frame-snapshot.d.ts.map +1 -1
  102. package/dist/record/frame.d.ts +5 -4
  103. package/dist/record/frame.d.ts.map +1 -1
  104. package/dist/record/helpers.d.ts +2 -2
  105. package/dist/record/helpers.d.ts.map +1 -1
  106. package/dist/record/main-pass-geometry.d.ts +2 -2
  107. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  108. package/dist/record/main-pass-material.d.ts +7 -9
  109. package/dist/record/main-pass-material.d.ts.map +1 -1
  110. package/dist/record/main-pass-sprite-draws.d.ts +3 -16
  111. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  112. package/dist/record/main-pass.d.ts.map +1 -1
  113. package/dist/record/prepared-material-bindings.d.ts +2 -3
  114. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  115. package/dist/record/render-context.d.ts +4 -2
  116. package/dist/record/render-context.d.ts.map +1 -1
  117. package/dist/record/shadow-pass.d.ts +2 -2
  118. package/dist/record/shadow-pass.d.ts.map +1 -1
  119. package/dist/record/sprite-instance-buffer.d.ts +31 -0
  120. package/dist/record/sprite-instance-buffer.d.ts.map +1 -0
  121. package/dist/record/target-capture-graph.d.ts +19 -0
  122. package/dist/record/target-capture-graph.d.ts.map +1 -0
  123. package/dist/record/typed-frame-graph.d.ts +2 -13
  124. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  125. package/dist/record/view-ubo.d.ts.map +1 -1
  126. package/dist/record/volume-params.d.ts +2 -2
  127. package/dist/record/volume-params.d.ts.map +1 -1
  128. package/dist/recovery/render-system-candidate.d.ts +5 -12
  129. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  130. package/dist/render-contract.d.ts +4 -1
  131. package/dist/render-contract.d.ts.map +1 -1
  132. package/dist/render-system-extract-tail.d.ts.map +1 -1
  133. package/dist/render-system-extract.d.ts +2 -0
  134. package/dist/render-system-extract.d.ts.map +1 -1
  135. package/dist/render-system-presentation.d.ts +2 -2
  136. package/dist/render-system-presentation.d.ts.map +1 -1
  137. package/dist/render-system-projections.d.ts +3 -2
  138. package/dist/render-system-projections.d.ts.map +1 -1
  139. package/dist/render-system-residency.d.ts +2 -2
  140. package/dist/render-system-residency.d.ts.map +1 -1
  141. package/dist/render-system.d.ts +3 -7
  142. package/dist/render-system.d.ts.map +1 -1
  143. package/dist/scene/render-scene.d.ts +8 -4
  144. package/dist/scene/render-scene.d.ts.map +1 -1
  145. package/dist/scene/render-source.d.ts +42 -0
  146. package/dist/scene/render-source.d.ts.map +1 -0
  147. package/dist/scene/source-systems.d.ts +6 -0
  148. package/dist/scene/source-systems.d.ts.map +1 -0
  149. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  150. package/dist/systems/skin-palette-types.d.ts +10 -0
  151. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  152. package/dist/systems/transparent-dispatch.d.ts +2 -2
  153. package/dist/systems/transparent-dispatch.d.ts.map +1 -1
  154. package/dist/systems/transparent-sort-config.d.ts +2 -1
  155. package/dist/systems/transparent-sort-config.d.ts.map +1 -1
  156. package/dist/temporal/index.mjs +6 -7
  157. package/dist/typed-shadow-passes.d.ts.map +1 -1
  158. package/package.json +21 -21
  159. package/src/__tests__/auto-exposure-transaction.integration.test.ts +4 -9
  160. package/src/__tests__/device-feature-admission.unit.test.ts +3 -2
  161. package/src/__tests__/device-scope-lifecycle.unit.test.ts +26 -0
  162. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +3 -4
  163. package/src/__tests__/gpu-driven-production.integration.test.ts +18 -1
  164. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +23 -0
  165. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +4 -1
  166. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +0 -1
  167. package/src/__tests__/material-vertex-texture.browser.test.ts +7 -0
  168. package/src/__tests__/material-vertex-texture.dawn.test.ts +7 -0
  169. package/src/__tests__/material-vertex-texture.fixture.ts +31 -0
  170. package/src/__tests__/materials-standard-contract.unit.test.ts +0 -1
  171. package/src/__tests__/physical-clearcoat.integration.test.ts +23 -23
  172. package/src/__tests__/physical-material-budget.unit.test.ts +40 -0
  173. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +5 -0
  174. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  175. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +107 -5
  176. package/src/__tests__/runtime-material-program.integration.test.ts +79 -0
  177. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +23 -3
  178. package/src/assembly/device-feature-admission.ts +2 -2
  179. package/src/assembly/host-contract.ts +5 -2
  180. package/src/assembly/recovery/renderer-recover.ts +9 -0
  181. package/src/assembly/render-target-host.ts +57 -47
  182. package/src/assembly/renderer-facade.ts +3 -1
  183. package/src/assembly/webgpu-ready.ts +1 -1
  184. package/src/assembly/webgpu-renderer.ts +101 -70
  185. package/src/assembly/webgpu-vertex-layouts.ts +2 -9
  186. package/src/cloud/frame-context.ts +2 -2
  187. package/src/device/device-scope.ts +3 -0
  188. package/src/device/gpu-residency.ts +26 -25
  189. package/src/errors/render.ts +3 -1
  190. package/src/extract/material-context.ts +21 -0
  191. package/src/features/host.ts +13 -5
  192. package/src/features/particle-mesh-layout.ts +25 -0
  193. package/src/features/plan.ts +2 -0
  194. package/src/features/types.ts +8 -0
  195. package/src/glyph-text-layout-system.ts +2 -2
  196. package/src/gpu-driven/production-raster.ts +9 -3
  197. package/src/gpu-driven/shadow-ownership.ts +3 -3
  198. package/src/gpu-driven/shadow-views.ts +27 -2
  199. package/src/gpu-driven/view-gpu.ts +5 -0
  200. package/src/ibl/frame-resources.ts +0 -1
  201. package/src/ibl/skylight-bind-group.ts +13 -25
  202. package/src/index.ts +9 -0
  203. package/src/instances.ts +9 -6
  204. package/src/pbr-pipeline.ts +15 -18
  205. package/src/pipeline/standard-output/lut-gpu.ts +3 -3
  206. package/src/pipeline/standard-output/resources.ts +2 -2
  207. package/src/publication/__tests__/derived-geometry.integration.test.ts +177 -0
  208. package/src/publication/__tests__/features.integration.test.ts +173 -0
  209. package/src/publication/__tests__/programs.unit.test.ts +25 -0
  210. package/src/publication/__tests__/publication.integration.test.ts +415 -0
  211. package/src/publication/__tests__/targets.integration.test.ts +120 -0
  212. package/src/publication/__tests__/video.browser.test.ts +50 -0
  213. package/src/publication/contract.ts +104 -0
  214. package/src/publication/dependencies.ts +54 -0
  215. package/src/publication/prepare-geometry.ts +94 -0
  216. package/src/publication/programs.ts +45 -0
  217. package/src/publication/publisher.ts +645 -0
  218. package/src/publication/receiver.ts +297 -0
  219. package/src/publication/resource-scope.ts +31 -0
  220. package/src/publication/targets.ts +182 -0
  221. package/src/publication/video.ts +33 -0
  222. package/src/record/__tests__/material-bg-cache.unit.test.ts +15 -1
  223. package/src/record/__tests__/prepared-runtime-material.integration.test.ts +134 -0
  224. package/src/record/__tests__/sprite-material-abi.unit.test.ts +8 -10
  225. package/src/record/__tests__/video-texture-view.unit.test.ts +9 -9
  226. package/src/record/fold-buckets.ts +8 -5
  227. package/src/record/frame-snapshot.ts +6 -6
  228. package/src/record/frame.ts +87 -28
  229. package/src/record/helpers.ts +2 -2
  230. package/src/record/main-pass-geometry.ts +21 -2
  231. package/src/record/main-pass-material.ts +37 -33
  232. package/src/record/main-pass-sprite-draws.ts +6 -152
  233. package/src/record/main-pass.ts +8 -7
  234. package/src/record/prepared-material-bindings.ts +16 -37
  235. package/src/record/render-context.ts +4 -2
  236. package/src/record/shadow-pass.ts +65 -7
  237. package/src/record/sprite-instance-buffer.ts +155 -0
  238. package/src/record/target-capture-graph.ts +258 -0
  239. package/src/record/typed-frame-graph.ts +11 -207
  240. package/src/record/view-ubo.ts +1 -1
  241. package/src/record/volume-params.ts +6 -6
  242. package/src/recovery/render-system-candidate.ts +10 -18
  243. package/src/render-contract.ts +6 -1
  244. package/src/render-system-extract-tail.ts +48 -23
  245. package/src/render-system-extract.ts +11 -2
  246. package/src/render-system-presentation.ts +2 -2
  247. package/src/render-system-projections.ts +3 -2
  248. package/src/render-system-residency.ts +2 -2
  249. package/src/render-system.ts +82 -54
  250. package/src/scene/render-scene.ts +110 -100
  251. package/src/scene/render-source.ts +90 -0
  252. package/src/scene/source-systems.ts +33 -0
  253. package/src/systems/skin-palette-allocator.ts +1 -0
  254. package/src/systems/skin-palette-types.ts +11 -0
  255. package/src/systems/transparent-dispatch.ts +2 -2
  256. package/src/systems/transparent-sort-config.ts +3 -1
  257. package/src/typed-shadow-passes.ts +36 -5
  258. package/dist/chunk-BLXGONR2.mjs.map +0 -1
  259. package/dist/chunk-ELJFJ2BU.mjs.map +0 -1
  260. package/dist/chunk-ILQ5MEZF.mjs +0 -32
  261. package/dist/chunk-ILQ5MEZF.mjs.map +0 -1
  262. package/dist/chunk-MKDIT2EM.mjs.map +0 -1
  263. package/dist/chunk-OCDLEI6I.mjs.map +0 -1
  264. package/dist/chunk-ODZWR6JQ.mjs.map +0 -1
  265. package/dist/chunk-RXZ3RGOU.mjs +0 -53
  266. package/dist/chunk-RXZ3RGOU.mjs.map +0 -1
  267. package/dist/chunk-SHFUFJ5N.mjs +0 -1781
  268. package/dist/chunk-SHFUFJ5N.mjs.map +0 -1
  269. package/dist/chunk-UI5SFODX.mjs.map +0 -1
  270. package/dist/chunk-UINHLCZA.mjs +0 -2389
  271. package/dist/chunk-UINHLCZA.mjs.map +0 -1
  272. package/dist/chunk-WEX45HKH.mjs +0 -1524
  273. package/dist/chunk-WEX45HKH.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;
@@ -1874,7 +1935,6 @@ function ensureSpotShadowMaterialBg(c: _InternalRenderPipelineContext): BindGrou
1874
1935
  prefilterView: fb.prefilterView,
1875
1936
  prefilterSampler: fb.sampler,
1876
1937
  brdfLutView: fb.brdfLutView,
1877
- brdfLutSampler: fb.sampler,
1878
1938
  intensityBuffer: fb.intensityBuffer,
1879
1939
  },
1880
1940
  transmissionBackdropAvailable(runtime.device.limits.maxSampledTexturesPerShaderStage)
@@ -1956,7 +2016,6 @@ function ensureGpuDrivenAlphaMaskMaterialBg(
1956
2016
  prefilterView: fallback.prefilterView,
1957
2017
  prefilterSampler: fallback.sampler,
1958
2018
  brdfLutView: fallback.brdfLutView,
1959
- brdfLutSampler: fallback.sampler,
1960
2019
  intensityBuffer: fallback.intensityBuffer,
1961
2020
  } satisfies import('../ibl/skylight-bind-group').SkylightBindGroupResources;
1962
2021
  const deps: PerSubmeshMaterialBgDeps = {
@@ -2022,7 +2081,6 @@ function ensureGpuDrivenCustomShadowMaterialBg(
2022
2081
  prefilterView: fallback.prefilterView,
2023
2082
  prefilterSampler: fallback.sampler,
2024
2083
  brdfLutView: fallback.brdfLutView,
2025
- brdfLutSampler: fallback.sampler,
2026
2084
  intensityBuffer: fallback.intensityBuffer,
2027
2085
  } satisfies import('../ibl/skylight-bind-group').SkylightBindGroupResources;
2028
2086
  const deps: PerSubmeshMaterialBgDeps = {
@@ -0,0 +1,155 @@
1
+ import { type Buffer, RhiError } from '@forgeax/engine-rhi';
2
+ import { GpuBuffer } from '../gpu-resource';
3
+ import {
4
+ GPU_BUFFER_USAGE_COPY_DST,
5
+ GPU_BUFFER_USAGE_STORAGE,
6
+ GPU_BUFFER_USAGE_UNIFORM,
7
+ } from '../gpu-usage';
8
+ import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
9
+ import type { SpriteInstancesSnapshot } from '../render-system-extract';
10
+ import { worldEntityKey } from './frame-snapshot';
11
+ import type { _InternalRenderPipelineContext } from './render-context';
12
+
13
+ export type { SpriteInstancesSnapshot } from '../render-system-extract';
14
+
15
+ /**
16
+ * Build the interleaved sprite instance payload consumed by this record owner.
17
+ * The extract stage validates the two packed arrays before they reach this
18
+ * function, so the record path has one source of count-mismatch errors.
19
+ */
20
+ export function interleaveSpriteInstanceBuffer(
21
+ transforms: Float32Array,
22
+ regions: Float32Array,
23
+ includePrevious = false,
24
+ ): Float32Array {
25
+ const count = transforms.length / 16;
26
+ const stride = includePrevious ? 36 : 20;
27
+ const regionOffset = includePrevious ? 32 : 16;
28
+ const out = new Float32Array(count * stride);
29
+ for (let i = 0; i < count; i++) {
30
+ const dstBase = i * stride;
31
+ const transformBase = i * 16;
32
+ const regionBase = i * 4;
33
+ for (let k = 0; k < 16; k++) out[dstBase + k] = transforms[transformBase + k] ?? 0;
34
+ if (includePrevious) {
35
+ for (let k = 0; k < 16; k++) {
36
+ out[dstBase + 16 + k] = transforms[transformBase + k] ?? 0;
37
+ }
38
+ }
39
+ for (let k = 0; k < 4; k++) {
40
+ out[dstBase + regionOffset + k] = regions[regionBase + k] ?? 0;
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+
46
+ /**
47
+ * Check whether the existing per-entity sprite buffer still matches the
48
+ * extract snapshot and interleaved byte count.
49
+ */
50
+ export function spriteInstancesCacheHit(
51
+ entry: InstanceBufferCacheEntry | undefined,
52
+ snapshot: SpriteInstancesSnapshot,
53
+ requestedBytes: number,
54
+ ): boolean {
55
+ return (
56
+ entry !== undefined &&
57
+ entry.uploadedArchVersion === snapshot.archVersion &&
58
+ entry.uploadedByteLength === requestedBytes
59
+ );
60
+ }
61
+
62
+ /**
63
+ * feat-20260704 M3/w19: resolve the interleaved SpriteInstances (@group(3))
64
+ * buffer + instanceCount for a sprite entity in the LDR blend sub-pass,
65
+ * extracted verbatim from recordSpriteEntityDraws. Uploads the interleaved
66
+ * mat4 + per-instance UV region transforms (cache-keyed on the snapshot); on
67
+ * over-cap fires the structured limit-exceeded error and leaves the passed-in
68
+ * fallback buffer/count. Returns the resolved (or unchanged) buffer + count.
69
+ *
70
+ * @internal
71
+ */
72
+ export function resolveSpriteInstancesBuffer(
73
+ c: Pick<_InternalRenderPipelineContext, 'runtime' | 'frameState'>,
74
+ spriteEntry: _InternalRenderPipelineContext['validatedOrdered'][number],
75
+ fallbackBuffer: Buffer,
76
+ fallbackCount: number,
77
+ ): { buffer: Buffer; count: number } {
78
+ const { runtime, frameState } = c;
79
+ let buffer = fallbackBuffer;
80
+ let count = fallbackCount;
81
+ const spriteInstancesSnap: SpriteInstancesSnapshot | undefined =
82
+ spriteEntry.source.spriteInstances;
83
+ if (spriteInstancesSnap !== undefined) {
84
+ if (spriteInstancesSnap.instanceCount === 0) return { buffer, count: 0 };
85
+ const uniformFallback = runtime.device.caps.storageBuffer === false;
86
+ const interleaved = interleaveSpriteInstanceBuffer(
87
+ spriteInstancesSnap.transforms,
88
+ spriteInstancesSnap.regions,
89
+ !uniformFallback,
90
+ );
91
+ const requestedBytes = interleaved.byteLength;
92
+ const cap = runtime.device.limits.maxStorageBufferBindingSize;
93
+ if (typeof cap === 'number' && requestedBytes > cap) {
94
+ runtime.errorRegistry.fire(
95
+ new RhiError({
96
+ code: 'limit-exceeded',
97
+ expected: `requestedBytes (${requestedBytes}) <= maxStorageBufferBindingSize (${cap})`,
98
+ hint: 'reduce SpriteInstances instance count to fit within device.limits.maxStorageBufferBindingSize (144 bytes per instance: current mat4 64B + previous mat4 64B + region 16B)',
99
+ detail: {
100
+ maxStorageBufferBindingSize: cap,
101
+ requestedBytes,
102
+ },
103
+ }),
104
+ );
105
+ } else {
106
+ const cachedSpriteInst = frameState.instanceBuffers.get(
107
+ worldEntityKey(spriteEntry.source.worldId, spriteInstancesSnap.cacheKey),
108
+ );
109
+ let activeSpriteInst: InstanceBufferCacheEntry | null = null;
110
+ if (spriteInstancesCacheHit(cachedSpriteInst, spriteInstancesSnap, requestedBytes)) {
111
+ activeSpriteInst = cachedSpriteInst ?? null;
112
+ } else if (requestedBytes > 0) {
113
+ const bufRes = runtime.device.createBuffer({
114
+ size: requestedBytes,
115
+ usage:
116
+ (uniformFallback ? GPU_BUFFER_USAGE_UNIFORM : GPU_BUFFER_USAGE_STORAGE) |
117
+ GPU_BUFFER_USAGE_COPY_DST,
118
+ mappedAtCreation: false,
119
+ });
120
+ if (!bufRes.ok) {
121
+ runtime.errorRegistry.fire(bufRes.error);
122
+ } else {
123
+ if (cachedSpriteInst !== undefined && !cachedSpriteInst.buffer.isDestroyed) {
124
+ const r = cachedSpriteInst.buffer.destroy();
125
+ if (!r.ok) runtime.errorRegistry.fire(r.error);
126
+ }
127
+ const newBuf = new GpuBuffer(runtime.device, bufRes.value);
128
+ activeSpriteInst = {
129
+ buffer: newBuf,
130
+ uploadedArchVersion: spriteInstancesSnap.archVersion,
131
+ uploadedByteLength: requestedBytes,
132
+ };
133
+ frameState.instanceBuffers.set(
134
+ worldEntityKey(spriteEntry.source.worldId, spriteInstancesSnap.cacheKey),
135
+ activeSpriteInst,
136
+ );
137
+ }
138
+ }
139
+ if (activeSpriteInst !== null && requestedBytes > 0) {
140
+ const writeRes = runtime.device.queue.writeBuffer(
141
+ activeSpriteInst.buffer.handle,
142
+ 0,
143
+ interleaved,
144
+ );
145
+ if (!writeRes.ok) {
146
+ runtime.errorRegistry.fire(writeRes.error);
147
+ } else {
148
+ buffer = activeSpriteInst.buffer.handle;
149
+ count = spriteInstancesSnap.instanceCount;
150
+ }
151
+ }
152
+ }
153
+ }
154
+ return { buffer, count };
155
+ }
@@ -0,0 +1,258 @@
1
+ import type { RenderGraphBuilder, RenderGraphError } from '@forgeax/engine-render-graph';
2
+ import type { Texture, TextureView } from '@forgeax/engine-rhi';
3
+ import { ok, type Result } from '@forgeax/engine-types';
4
+ import type { RenderPipelineFrame } from '../render-pipeline';
5
+ import type { RenderTarget } from '../targets/contracts';
6
+ import { resolveRenderTargetMaterialSource } from '../targets/material-source';
7
+ import type { RenderTargetPhysical } from '../targets/physical';
8
+ import { encodeMainPass } from './main-pass';
9
+ import type { _InternalRenderPipelineContext } from './render-context';
10
+ import type { ReflectionProbeGraphState } from './typed-frame-graph';
11
+
12
+ export interface CubeCaptureGraphWork {
13
+ readonly target: RenderTarget;
14
+ readonly faceIndex: number;
15
+ readonly physical: RenderTargetPhysical;
16
+ readonly faceCamera: import('../render-contract').CameraSnapshot;
17
+ readonly viewBindGroupDynamicOffset: number;
18
+ }
19
+
20
+ export interface CubeCaptureGraphState {
21
+ work: readonly CubeCaptureGraphWork[];
22
+ reflectionProbes?: ReflectionProbeGraphState;
23
+ }
24
+
25
+ function renderTargetMipCount(work: CubeCaptureGraphWork): number {
26
+ const { descriptor } = work.physical;
27
+ return descriptor.mipLevels === 1
28
+ ? 1
29
+ : Math.floor(Math.log2(Math.max(descriptor.width, descriptor.height))) + 1;
30
+ }
31
+
32
+ export function addTargetCaptureGraphPasses(
33
+ builder: RenderGraphBuilder<RenderPipelineFrame>,
34
+ state: CubeCaptureGraphState,
35
+ ): Result<void, RenderGraphError> {
36
+ for (let index = 0; index < state.work.length; index += 1) {
37
+ const slot = index;
38
+ const initial = state.work[slot];
39
+ if (initial === undefined) continue;
40
+ const current = (): CubeCaptureGraphWork => state.work[slot] ?? initial;
41
+ const texture = builder.importTexture(
42
+ `cube-capture.${slot}.texture`,
43
+ {
44
+ format: initial.physical.descriptor.format,
45
+ size: {
46
+ width: initial.physical.descriptor.width,
47
+ height: initial.physical.descriptor.height,
48
+ depthOrArrayLayers:
49
+ initial.physical.descriptor.sampleCount === 4 ||
50
+ initial.physical.descriptor.shape === '2d'
51
+ ? 1
52
+ : 6,
53
+ },
54
+ mipLevelCount:
55
+ initial.physical.descriptor.sampleCount === 4 ? 1 : renderTargetMipCount(initial),
56
+ sampleCount: initial.physical.descriptor.sampleCount,
57
+ dimension: '2d',
58
+ usage: 0x10 | 0x04 | 0x01,
59
+ },
60
+ () => current().physical.colorTextures[current().faceIndex] ?? initial.physical.texture,
61
+ );
62
+ if (!texture.ok) return texture;
63
+ const color = builder.importView(
64
+ texture.value,
65
+ {
66
+ label: `cube-capture.${slot}.color`,
67
+ dimension: '2d',
68
+ baseMipLevel: 0,
69
+ mipLevelCount: 1,
70
+ baseArrayLayer: initial.physical.descriptor.sampleCount === 4 ? 0 : initial.faceIndex,
71
+ arrayLayerCount: 1,
72
+ },
73
+ () =>
74
+ current().physical.faceViews[current().faceIndex] ??
75
+ initial.physical.faceViews[initial.faceIndex] ??
76
+ initial.physical.view,
77
+ );
78
+ if (!color.ok) return color;
79
+ const depthTexture = builder.importTexture(
80
+ `cube-capture.${slot}.depth-texture`,
81
+ {
82
+ format: 'depth24plus-stencil8',
83
+ size: {
84
+ width: initial.physical.descriptor.width,
85
+ height: initial.physical.descriptor.height,
86
+ depthOrArrayLayers:
87
+ initial.physical.descriptor.sampleCount === 4 ||
88
+ initial.physical.descriptor.shape === '2d'
89
+ ? 1
90
+ : 6,
91
+ },
92
+ mipLevelCount: 1,
93
+ sampleCount: initial.physical.descriptor.sampleCount,
94
+ dimension: '2d',
95
+ usage: 0x10,
96
+ },
97
+ () => current().physical.depthTextures[current().faceIndex] as Texture,
98
+ );
99
+ if (!depthTexture.ok) return depthTexture;
100
+ const depth = builder.importView(
101
+ depthTexture.value,
102
+ {
103
+ label: `cube-capture.${slot}.depth`,
104
+ dimension: '2d',
105
+ baseArrayLayer: initial.physical.descriptor.sampleCount === 4 ? 0 : initial.faceIndex,
106
+ arrayLayerCount: 1,
107
+ },
108
+ () => current().physical.depthViews[current().faceIndex] as TextureView,
109
+ );
110
+ if (!depth.ok) return depth;
111
+ const resolveTexture =
112
+ initial.physical.descriptor.sampleCount === 4
113
+ ? builder.importTexture(
114
+ `cube-capture.${slot}.resolve-texture`,
115
+ {
116
+ format: initial.physical.descriptor.format,
117
+ size: {
118
+ width: initial.physical.descriptor.width,
119
+ height: initial.physical.descriptor.height,
120
+ depthOrArrayLayers: initial.physical.descriptor.shape === 'cube' ? 6 : 1,
121
+ },
122
+ mipLevelCount: renderTargetMipCount(initial),
123
+ sampleCount: 1,
124
+ dimension: '2d',
125
+ usage: 0x10 | 0x04 | 0x01,
126
+ },
127
+ () =>
128
+ current().physical.resolveTexture ??
129
+ initial.physical.resolveTexture ??
130
+ initial.physical.texture,
131
+ )
132
+ : undefined;
133
+ if (resolveTexture !== undefined && !resolveTexture.ok) return resolveTexture;
134
+ const resolve =
135
+ resolveTexture === undefined
136
+ ? undefined
137
+ : builder.importView(
138
+ resolveTexture.value,
139
+ {
140
+ label: `cube-capture.${slot}.resolve`,
141
+ dimension: '2d',
142
+ baseMipLevel: 0,
143
+ mipLevelCount: 1,
144
+ baseArrayLayer: initial.faceIndex,
145
+ arrayLayerCount: 1,
146
+ },
147
+ () =>
148
+ current().physical.resolveFaceViews[current().faceIndex] ??
149
+ initial.physical.resolveFaceViews[initial.faceIndex] ??
150
+ initial.physical.resolveView,
151
+ );
152
+ if (resolve !== undefined && !resolve.ok) return resolve;
153
+ const added = builder.addRasterPass(`cube-capture-face.${slot}`, {
154
+ accesses: [
155
+ { resource: color.value, usage: 'color-attachment' },
156
+ { resource: depth.value, usage: 'depth-stencil-write' },
157
+ ...(resolve === undefined
158
+ ? []
159
+ : [{ resource: resolve.value, usage: 'color-attachment' as const }]),
160
+ ],
161
+ colorAttachments: [
162
+ {
163
+ view: color.value,
164
+ ...(resolve === undefined ? {} : { resolveTarget: resolve.value }),
165
+ loadOp: 'clear',
166
+ storeOp: 'store',
167
+ clearValue: () => {
168
+ const clear =
169
+ current().physical.descriptor.shape === '2d'
170
+ ? current().faceCamera.clearColor
171
+ : [0, 0, 0, 1];
172
+ return { r: clear[0] ?? 0, g: clear[1] ?? 0, b: clear[2] ?? 0, a: clear[3] ?? 1 };
173
+ },
174
+ },
175
+ ],
176
+ depthStencilAttachment: {
177
+ view: depth.value,
178
+ depthClearValue: 1,
179
+ depthLoadOp: 'clear',
180
+ depthStoreOp: 'store',
181
+ stencilClearValue: 0,
182
+ stencilLoadOp: 'clear',
183
+ stencilStoreOp: 'store',
184
+ },
185
+ encode: ({ pass, frame, resources }) => {
186
+ const internal = frame as _InternalRenderPipelineContext;
187
+ const captureColor = resources.textureView(color.value);
188
+ if (!captureColor.ok) return;
189
+ const captureDepth = resources.textureView(depth.value);
190
+ if (!captureDepth.ok) return;
191
+ const captureResolve =
192
+ resolve === undefined ? undefined : resources.textureView(resolve.value);
193
+ if (resolve !== undefined && (captureResolve === undefined || !captureResolve.ok)) return;
194
+ const captureResolveView = captureResolve?.ok ? captureResolve.value : null;
195
+ const selfSampling = new Map<number, Set<number>>();
196
+ for (const row of internal.validatedOrdered) {
197
+ for (const material of row.source.materials) {
198
+ if (
199
+ ![...(material.textureSources?.values() ?? [])].some(
200
+ (source) => resolveRenderTargetMaterialSource(source)?.target === current().target,
201
+ )
202
+ )
203
+ continue;
204
+ let handles = selfSampling.get(row.renderableIndex);
205
+ if (handles === undefined) {
206
+ handles = new Set();
207
+ selfSampling.set(row.renderableIndex, handles);
208
+ }
209
+ handles.add(material.materialHandle ?? 0);
210
+ }
211
+ }
212
+ const captureContext = {
213
+ ...internal,
214
+ camera: current().faceCamera,
215
+ targetW: current().physical.descriptor.width,
216
+ targetH: current().physical.descriptor.height,
217
+ dispatch: (internal.captureDispatch ?? internal.dispatch).filter(
218
+ (entry) => !selfSampling.get(entry.renderableIndex)?.has(entry.materialHandle),
219
+ ),
220
+ foldDispatchPlan: null,
221
+
222
+ // Cube faces are raw scene captures, not display-camera HDR output.
223
+ // Select the PSO against the actual target attachment so an LDR
224
+ // cube target is rendered by a compatible forward pipeline.
225
+ tonemapActive: false,
226
+ skyboxActive: false,
227
+ transparentColorFormat: initial.physical.descriptor.format as GPUTextureFormat,
228
+ msaaActive: initial.physical.descriptor.sampleCount === 4,
229
+ viewBindGroupDynamicOffset: current().viewBindGroupDynamicOffset,
230
+ geometryColorView: captureColor.value,
231
+ geometryDepthView: captureDepth.value,
232
+ geometryColorResolveView: captureResolveView,
233
+ splitLdrSprite: false,
234
+ ldrSpritePassView: null,
235
+ };
236
+ delete captureContext.gpuDrivenDrawKeys;
237
+ delete captureContext.gpuDrivenStandardPbrFrameResources;
238
+ encodeMainPass(
239
+ captureContext,
240
+ pass,
241
+ { LightMode: ['Forward'] },
242
+ {
243
+ colorViews: [captureColor.value],
244
+ colorFormats: [initial.physical.descriptor.format as GPUTextureFormat],
245
+ depthView: captureDepth.value,
246
+ passKind: 'forward',
247
+ clearColor:
248
+ current().physical.descriptor.shape === '2d'
249
+ ? current().faceCamera.clearColor
250
+ : [0, 0, 0, 1],
251
+ },
252
+ );
253
+ },
254
+ });
255
+ if (!added.ok) return added;
256
+ }
257
+ return ok(undefined);
258
+ }