@forgeax/engine-render 0.1.23 → 0.1.24

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 (271) hide show
  1. package/README.md +131 -9
  2. package/dist/assembly/bundler-contract.d.ts +9 -0
  3. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +8 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  7. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +7 -243
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +3 -2
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  13. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  14. package/dist/assembly/material-shader-policy.d.ts +168 -0
  15. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  17. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  18. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  19. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  20. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  21. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  22. package/dist/assembly/recovery/generation.d.ts +46 -0
  23. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  24. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  26. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  27. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  28. package/dist/assembly/renderer-facade.d.ts +10 -0
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  30. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  31. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  32. package/dist/assembly/renderer-helpers.d.ts +19 -0
  33. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  34. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  35. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  36. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  37. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  38. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  39. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready.d.ts +55 -0
  41. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  43. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  44. package/dist/authoring.mjs +1 -1
  45. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  46. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  47. package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
  48. package/dist/chunk-KZFTK6DO.mjs.map +1 -0
  49. package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
  50. package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
  51. package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
  52. package/dist/chunk-RR5L4SRS.mjs.map +1 -0
  53. package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
  54. package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
  55. package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
  56. package/dist/chunk-UQYS7FRM.mjs.map +1 -0
  57. package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
  58. package/dist/chunk-YKB5DQV7.mjs.map +1 -0
  59. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
  60. package/dist/chunk-YRUYGA4A.mjs.map +1 -0
  61. package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
  62. package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
  63. package/dist/construct-renderer.mjs +19841 -16190
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/device-scope.d.ts +2 -0
  66. package/dist/device/device-scope.d.ts.map +1 -1
  67. package/dist/device/gpu-residency.d.ts +24 -4
  68. package/dist/device/gpu-residency.d.ts.map +1 -1
  69. package/dist/environment/frame.d.ts +5 -0
  70. package/dist/environment/frame.d.ts.map +1 -1
  71. package/dist/environment/inspection.d.ts +7 -0
  72. package/dist/environment/inspection.d.ts.map +1 -1
  73. package/dist/environment/lifecycle.d.ts +20 -2
  74. package/dist/environment/lifecycle.d.ts.map +1 -1
  75. package/dist/errors/recover.d.ts +58 -2
  76. package/dist/errors/recover.d.ts.map +1 -1
  77. package/dist/errors/render.d.ts +13 -0
  78. package/dist/errors/render.d.ts.map +1 -1
  79. package/dist/extract/contracts.d.ts +899 -0
  80. package/dist/extract/contracts.d.ts.map +1 -0
  81. package/dist/features/host.d.ts +2 -0
  82. package/dist/features/host.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +3 -0
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/prepared-graphics-store.d.ts +2 -0
  86. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  87. package/dist/gpu-driven/production-raster.d.ts +28 -0
  88. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  89. package/dist/gpu-scene.d.ts +1 -0
  90. package/dist/gpu-scene.d.ts.map +1 -1
  91. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  92. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +6 -6
  98. package/dist/internal.mjs +6 -6
  99. package/dist/light-buffer-layout.d.ts +3 -0
  100. package/dist/light-buffer-layout.d.ts.map +1 -1
  101. package/dist/materials.d.ts +34 -10
  102. package/dist/materials.d.ts.map +1 -1
  103. package/dist/mesh-material-bindings.d.ts +56 -0
  104. package/dist/mesh-material-bindings.d.ts.map +1 -1
  105. package/dist/occlusion-proxy.d.ts +4 -0
  106. package/dist/occlusion-proxy.d.ts.map +1 -0
  107. package/dist/pbr-pipeline.d.ts +32 -9
  108. package/dist/pbr-pipeline.d.ts.map +1 -1
  109. package/dist/points-lines/prepare.d.ts +3 -0
  110. package/dist/points-lines/prepare.d.ts.map +1 -1
  111. package/dist/points-lines/standard-owner.d.ts +33 -0
  112. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  113. package/dist/record/frame-snapshot.d.ts +5 -0
  114. package/dist/record/frame-snapshot.d.ts.map +1 -1
  115. package/dist/record/frame-targets.d.ts.map +1 -1
  116. package/dist/record/frame.d.ts +14 -2
  117. package/dist/record/frame.d.ts.map +1 -1
  118. package/dist/record/main-pass-geometry.d.ts +14 -1
  119. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  120. package/dist/record/main-pass-material.d.ts +7 -17
  121. package/dist/record/main-pass-material.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +11 -3
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/recovery-pipeline.d.ts +30 -0
  126. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  127. package/dist/record/render-context.d.ts +26 -4
  128. package/dist/record/render-context.d.ts.map +1 -1
  129. package/dist/record/typed-frame-graph.d.ts +1 -1
  130. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  131. package/dist/recovery/render-system-candidate.d.ts +65 -0
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  133. package/dist/render-contract.d.ts +64 -7
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-system-extract-tail.d.ts +12 -0
  136. package/dist/render-system-extract-tail.d.ts.map +1 -0
  137. package/dist/render-system-extract.d.ts +156 -22
  138. package/dist/render-system-extract.d.ts.map +1 -1
  139. package/dist/render-system.d.ts +128 -4
  140. package/dist/render-system.d.ts.map +1 -1
  141. package/dist/scene/render-scene.d.ts +19 -0
  142. package/dist/scene/render-scene.d.ts.map +1 -1
  143. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  144. package/dist/temporal/index.mjs +6 -6
  145. package/dist/temporal/taa-history-store.d.ts +15 -1
  146. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  147. package/dist/temporal/transaction.d.ts +5 -0
  148. package/dist/temporal/transaction.d.ts.map +1 -1
  149. package/package.json +20 -20
  150. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  151. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  152. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  153. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  154. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  155. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  156. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  157. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  158. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  159. package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
  160. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  161. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  162. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  163. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  164. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  165. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  166. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  167. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  168. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  169. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  170. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +85 -18
  171. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  172. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  173. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  174. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  175. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  176. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  177. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  178. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  179. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  180. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  181. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  182. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  183. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  184. package/src/assembly/bundler-contract.ts +9 -0
  185. package/src/assembly/device-feature-admission.ts +15 -5
  186. package/src/assembly/extended-lighting-fallback.ts +216 -0
  187. package/src/assembly/factory.ts +81 -10088
  188. package/src/assembly/material/assembly.ts +18 -2
  189. package/src/assembly/material/standard-layer-projection.ts +16 -0
  190. package/src/assembly/material-shader-policy.ts +854 -0
  191. package/src/assembly/material-shader-prewarm.ts +88 -0
  192. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  193. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  194. package/src/assembly/recovery/generation.ts +141 -0
  195. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  196. package/src/assembly/recovery/renderer-recover.ts +906 -0
  197. package/src/assembly/renderer-facade.ts +342 -0
  198. package/src/assembly/renderer-frame-transaction.ts +45 -0
  199. package/src/assembly/renderer-helpers.ts +99 -0
  200. package/src/assembly/renderer-lifecycle.ts +159 -0
  201. package/src/assembly/shader-prewarm-policy.ts +157 -0
  202. package/src/assembly/webgpu-ready-contract.ts +14 -0
  203. package/src/assembly/webgpu-ready.ts +4079 -0
  204. package/src/assembly/webgpu-renderer.ts +4095 -0
  205. package/src/components/camera.ts +1 -1
  206. package/src/debug-draw-glue.ts +1 -1
  207. package/src/device/device-scope.ts +35 -6
  208. package/src/device/gpu-residency.ts +194 -26
  209. package/src/environment/frame.ts +6 -0
  210. package/src/environment/inspection.ts +10 -0
  211. package/src/environment/lifecycle.ts +114 -8
  212. package/src/errors/recover.ts +76 -5
  213. package/src/errors/render.ts +17 -0
  214. package/src/extract/contracts.ts +974 -0
  215. package/src/features/host.ts +16 -0
  216. package/src/features/prepared-gpu-work.ts +20 -2
  217. package/src/features/prepared-graphics-store.ts +17 -0
  218. package/src/gpu-driven/production-raster.ts +97 -2
  219. package/src/gpu-scene.ts +3 -2
  220. package/src/ibl/IblPipelineCache.ts +134 -50
  221. package/src/ibl/skylight-bind-group.ts +26 -0
  222. package/src/index.ts +9 -1
  223. package/src/light-buffer-layout.ts +3 -0
  224. package/src/materials.ts +262 -45
  225. package/src/mesh-material-bindings.ts +59 -0
  226. package/src/occlusion-proxy.ts +64 -0
  227. package/src/pbr-pipeline.ts +139 -15
  228. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  229. package/src/points-lines/prepare.ts +9 -0
  230. package/src/points-lines/standard-owner.ts +403 -0
  231. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  232. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  233. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  234. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  235. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  236. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  237. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  238. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  239. package/src/record/frame-snapshot.ts +15 -0
  240. package/src/record/frame-targets.ts +11 -1
  241. package/src/record/frame.ts +6 -2
  242. package/src/record/helpers.ts +1 -1
  243. package/src/record/main-pass-geometry.ts +230 -5
  244. package/src/record/main-pass-material.ts +316 -209
  245. package/src/record/main-pass-sprite-draws.ts +6 -7
  246. package/src/record/main-pass.ts +94 -6
  247. package/src/record/mesh-ssbo.ts +32 -10
  248. package/src/record/recovery-pipeline.ts +695 -0
  249. package/src/record/render-context.ts +35 -4
  250. package/src/record/typed-frame-graph.ts +45 -1
  251. package/src/record/view-ubo.ts +3 -3
  252. package/src/recovery/render-system-candidate.ts +839 -0
  253. package/src/render-contract.ts +65 -7
  254. package/src/render-system-extract-tail.ts +2983 -0
  255. package/src/render-system-extract.ts +124 -2977
  256. package/src/render-system.ts +868 -659
  257. package/src/scene/render-scene.ts +124 -1
  258. package/src/temporal/frame-coordinator.ts +4 -1
  259. package/src/temporal/taa-history-store.ts +46 -2
  260. package/src/temporal/temporal-view.ts +1 -1
  261. package/src/temporal/transaction.ts +9 -0
  262. package/src/tilemap-chunk-extract-system.ts +1 -1
  263. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  264. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  265. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  266. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  267. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  268. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  269. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  270. package/dist/chunk-YALPS44A.mjs.map +0 -1
  271. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -27,7 +27,6 @@ import {
27
27
  type BindGroup,
28
28
  type BindGroupLayout,
29
29
  type Buffer,
30
- type CommandBuffer,
31
30
  err,
32
31
  ok,
33
32
  type RenderPipeline,
@@ -93,9 +92,8 @@ export interface IblPipelineError {
93
92
  * Per-DeviceScope-generation IBL pipeline cache instance.
94
93
  *
95
94
  * M3.5 (round-2 t52): the 4 pipeline slots are mutable so createIblPipelines
96
- * can fill them after construction. The 4 textures + their views are also
97
- * cached here so runIblPrecompute targets are stable across calls
98
- * (idempotent).
95
+ * can fill them after construction. The output textures and views are
96
+ * promoted here only after a completed precompute submission.
99
97
  */
100
98
  export interface IblPipelineCache {
101
99
  /** Reusable generation-scoped kernel state; per-probe outputs stay elsewhere. */
@@ -604,6 +602,17 @@ export interface RunIblPrecomputeOptions {
604
602
  readonly cubeVertexBuffer: Buffer;
605
603
  }
606
604
 
605
+ interface IblPrecomputeCandidate {
606
+ irradianceTexture: Texture;
607
+ irradianceView: TextureView;
608
+ irradianceFaceViews: ReadonlyArray<TextureView>;
609
+ prefilterTexture: Texture;
610
+ prefilterView: TextureView;
611
+ prefilterFaceViewsByMip: ReadonlyArray<ReadonlyArray<TextureView>>;
612
+ brdfLutTexture: Texture;
613
+ brdfLutView: TextureView;
614
+ }
615
+
607
616
  export interface IblPrecomputeError {
608
617
  readonly code: 'ibl-precompute-not-dispatched';
609
618
  readonly expected: string;
@@ -613,7 +622,9 @@ export interface IblPrecomputeError {
613
622
 
614
623
  /**
615
624
  * Execute the 4 IBL precompute passes (equirect-to-cube / irradiance /
616
- * prefilter / brdf-lut) in a single CommandEncoder and submit.
625
+ * prefilter / brdf-lut) as ordered, stage-bounded submissions. Outputs stay
626
+ * candidate-local until every queue completion fence and generation check
627
+ * pass.
617
628
  *
618
629
  * Counter invariant (AC-20, plan D-7 / N-3): the
619
630
  * `irradiance/prefilter/brdfLut BakeCount` counters are incremented
@@ -625,12 +636,19 @@ export interface IblPrecomputeError {
625
636
  * underlying device lacks queue.submit or one of the 4 pipelines was not
626
637
  * created (createIblPipelines must run first).
627
638
  */
628
- export function runIblPrecompute(
639
+ export async function runIblPrecompute(
629
640
  opts: RunIblPrecomputeOptions,
630
- ): Result<{ submitted: boolean }, IblPrecomputeError> {
641
+ ): Promise<Result<{ submitted: boolean }, IblPrecomputeError>> {
631
642
  const { device, scope } = opts;
632
643
  const cache = getOrCreateIblCache(scope);
633
644
  const outputFormat = cache.outputFormat ?? 'rgba16float';
645
+ const generation = scope.generation;
646
+ const candidate: Partial<IblPrecomputeCandidate> = {};
647
+ const fail = (stage: string, cause?: RhiError): Promise<Result<never, IblPrecomputeError>> => {
648
+ // Candidate textures are adopted by the DeviceScope immediately after
649
+ // allocation, so every failure is retired with the owning generation.
650
+ return Promise.resolve(err(badAlloc(stage, cause)));
651
+ };
634
652
 
635
653
  if (
636
654
  cache.equirectToCubePipeline === undefined ||
@@ -652,7 +670,7 @@ export function runIblPrecompute(
652
670
  // requires the TextureUsage::CopySrc bit on the source. Without it
653
671
  // Dawn fails-fast "usage doesn't include CopySrc".
654
672
 
655
- if (cache.irradianceTexture === undefined) {
673
+ {
656
674
  const textureResult = device.createTexture({
657
675
  label: 'ibl-irradiance-cube',
658
676
  size: { width: IRRADIANCE_SIZE, height: IRRADIANCE_SIZE, depthOrArrayLayers: 6 },
@@ -667,15 +685,15 @@ export function runIblPrecompute(
667
685
  viewFormats: [],
668
686
  textureBindingViewDimension: undefined,
669
687
  });
670
- if (!textureResult.ok) return err(badAlloc('irradiance-texture', textureResult.error));
671
- cache.irradianceTexture = textureResult.value;
688
+ if (!textureResult.ok) return fail('irradiance-texture', textureResult.error);
689
+ candidate.irradianceTexture = adoptIblTexture(scope, device, textureResult.value);
672
690
  const cubeViewResult = device.createTextureView(textureResult.value, {
673
691
  label: 'ibl-irradiance-cube-view',
674
692
  dimension: 'cube',
675
693
  arrayLayerCount: 6,
676
694
  });
677
- if (!cubeViewResult.ok) return err(badAlloc('irradiance-view', cubeViewResult.error));
678
- cache.irradianceView = cubeViewResult.value;
695
+ if (!cubeViewResult.ok) return fail('irradiance-view', cubeViewResult.error);
696
+ candidate.irradianceView = cubeViewResult.value;
679
697
  const faceViews: TextureView[] = [];
680
698
  for (let f = 0; f < 6; f++) {
681
699
  const viewResult = device.createTextureView(textureResult.value, {
@@ -684,13 +702,13 @@ export function runIblPrecompute(
684
702
  baseArrayLayer: f,
685
703
  arrayLayerCount: 1,
686
704
  });
687
- if (!viewResult.ok) return err(badAlloc('irradiance-face-view', viewResult.error));
705
+ if (!viewResult.ok) return fail('irradiance-face-view', viewResult.error);
688
706
  faceViews.push(viewResult.value);
689
707
  }
690
- cache.irradianceFaceViews = faceViews;
708
+ candidate.irradianceFaceViews = faceViews;
691
709
  }
692
710
 
693
- if (cache.prefilterTexture === undefined) {
711
+ {
694
712
  const textureResult = device.createTexture({
695
713
  label: 'ibl-prefilter-cube',
696
714
  size: { width: PREFILTER_SIZE, height: PREFILTER_SIZE, depthOrArrayLayers: 6 },
@@ -705,8 +723,8 @@ export function runIblPrecompute(
705
723
  viewFormats: [],
706
724
  textureBindingViewDimension: undefined,
707
725
  });
708
- if (!textureResult.ok) return err(badAlloc('prefilter-texture', textureResult.error));
709
- cache.prefilterTexture = textureResult.value;
726
+ if (!textureResult.ok) return fail('prefilter-texture', textureResult.error);
727
+ candidate.prefilterTexture = adoptIblTexture(scope, device, textureResult.value);
710
728
  const cubeViewResult = device.createTextureView(textureResult.value, {
711
729
  label: 'ibl-prefilter-cube-view',
712
730
  dimension: 'cube',
@@ -714,8 +732,8 @@ export function runIblPrecompute(
714
732
  baseMipLevel: 0,
715
733
  mipLevelCount: PREFILTER_MIP_LEVELS,
716
734
  });
717
- if (!cubeViewResult.ok) return err(badAlloc('prefilter-view', cubeViewResult.error));
718
- cache.prefilterView = cubeViewResult.value;
735
+ if (!cubeViewResult.ok) return fail('prefilter-view', cubeViewResult.error);
736
+ candidate.prefilterView = cubeViewResult.value;
719
737
  const mipViews: TextureView[][] = [];
720
738
  for (let m = 0; m < PREFILTER_MIP_LEVELS; m++) {
721
739
  const faces: TextureView[] = [];
@@ -728,15 +746,15 @@ export function runIblPrecompute(
728
746
  baseArrayLayer: f,
729
747
  arrayLayerCount: 1,
730
748
  });
731
- if (!viewResult.ok) return err(badAlloc('prefilter-face-view', viewResult.error));
749
+ if (!viewResult.ok) return fail('prefilter-face-view', viewResult.error);
732
750
  faces.push(viewResult.value);
733
751
  }
734
752
  mipViews.push(faces);
735
753
  }
736
- cache.prefilterFaceViewsByMip = mipViews;
754
+ candidate.prefilterFaceViewsByMip = mipViews;
737
755
  }
738
756
 
739
- if (cache.brdfLutTexture === undefined) {
757
+ {
740
758
  const textureResult = device.createTexture({
741
759
  label: 'ibl-brdf-lut',
742
760
  size: { width: BRDF_LUT_SIZE, height: BRDF_LUT_SIZE, depthOrArrayLayers: 1 },
@@ -751,14 +769,14 @@ export function runIblPrecompute(
751
769
  viewFormats: [],
752
770
  textureBindingViewDimension: undefined,
753
771
  });
754
- if (!textureResult.ok) return err(badAlloc('brdf-lut-texture', textureResult.error));
755
- cache.brdfLutTexture = textureResult.value;
772
+ if (!textureResult.ok) return fail('brdf-lut-texture', textureResult.error);
773
+ candidate.brdfLutTexture = adoptIblTexture(scope, device, textureResult.value);
756
774
  const viewResult = device.createTextureView(textureResult.value, {
757
775
  label: 'ibl-brdf-lut-view',
758
776
  dimension: '2d',
759
777
  });
760
- if (!viewResult.ok) return err(badAlloc('brdf-lut-view', viewResult.error));
761
- cache.brdfLutView = viewResult.value;
778
+ if (!viewResult.ok) return fail('brdf-lut-view', viewResult.error);
779
+ candidate.brdfLutView = viewResult.value;
762
780
  }
763
781
 
764
782
  // Shared sampler (filtering, linear-linear).
@@ -773,12 +791,35 @@ export function runIblPrecompute(
773
791
  addressModeV: 'clamp-to-edge',
774
792
  addressModeW: 'clamp-to-edge',
775
793
  });
776
- if (!samplerResult.ok) return err(badAlloc('sampler', samplerResult.error));
794
+ if (!samplerResult.ok) return fail('sampler', samplerResult.error);
777
795
  const sampler: Sampler = samplerResult.value;
778
796
 
779
- const encoderResult = device.createCommandEncoder({ label: 'ibl-precompute-encoder' });
780
- if (!encoderResult.ok) return err(badAlloc('encoder', encoderResult.error));
781
- const encoder: RhiCommandEncoder = encoderResult.value;
797
+ let encoderResult = device.createCommandEncoder({
798
+ label: 'ibl-precompute-encoder-equirect-to-cube',
799
+ });
800
+ if (!encoderResult.ok) return fail('encoder-equirect-to-cube', encoderResult.error);
801
+ let encoder: RhiCommandEncoder = encoderResult.value;
802
+
803
+ const submitStage = async (stage: string, createNextEncoder: boolean) => {
804
+ const finishRes = encoder.finish();
805
+ if (!finishRes.ok) return err(badAlloc(`${stage}-finish`, finishRes.error));
806
+ const submitRes = device.queue.submit([finishRes.value]);
807
+ if (!submitRes.ok) return err(badAlloc(`${stage}-submit`, submitRes.error));
808
+ try {
809
+ await device.queue.onSubmittedWorkDone();
810
+ } catch {
811
+ return err(badAlloc(`${stage}-fence`));
812
+ }
813
+ if (!scope.isAlive() || scope.generation !== generation) {
814
+ return err(badAlloc(`${stage}-device-scope-generation`));
815
+ }
816
+ if (createNextEncoder) {
817
+ encoderResult = device.createCommandEncoder({ label: `ibl-precompute-encoder-${stage}` });
818
+ if (!encoderResult.ok) return err(badAlloc(`${stage}-next-encoder`, encoderResult.error));
819
+ encoder = encoderResult.value;
820
+ }
821
+ return ok(true);
822
+ };
782
823
 
783
824
  const faceUniformsBgl = cache.faceUniformsBgl;
784
825
  const equirectGroup1Bgl = cache.equirectGroup1Bgl;
@@ -788,9 +829,14 @@ export function runIblPrecompute(
788
829
  const irradiancePipeline = cache.irradiancePipeline;
789
830
  const prefilterPipeline = cache.prefilterPipeline;
790
831
  const brdfLutPipeline = cache.brdfLutPipeline;
791
- const irrFaceViews = cache.irradianceFaceViews;
792
- const prefMipViews = cache.prefilterFaceViewsByMip;
793
- const brdfLutView = cache.brdfLutView;
832
+ const candidateIrradianceTexture = candidate.irradianceTexture;
833
+ const candidateIrradianceView = candidate.irradianceView;
834
+ const irrFaceViews = candidate.irradianceFaceViews;
835
+ const candidatePrefilterTexture = candidate.prefilterTexture;
836
+ const candidatePrefilterView = candidate.prefilterView;
837
+ const prefMipViews = candidate.prefilterFaceViewsByMip;
838
+ const candidateBrdfLutTexture = candidate.brdfLutTexture;
839
+ const brdfLutView = candidate.brdfLutView;
794
840
  if (
795
841
  faceUniformsBgl === undefined ||
796
842
  equirectGroup1Bgl === undefined ||
@@ -800,11 +846,16 @@ export function runIblPrecompute(
800
846
  irradiancePipeline === undefined ||
801
847
  prefilterPipeline === undefined ||
802
848
  brdfLutPipeline === undefined ||
849
+ candidateIrradianceTexture === undefined ||
850
+ candidateIrradianceView === undefined ||
803
851
  irrFaceViews === undefined ||
852
+ candidatePrefilterTexture === undefined ||
853
+ candidatePrefilterView === undefined ||
804
854
  prefMipViews === undefined ||
855
+ candidateBrdfLutTexture === undefined ||
805
856
  brdfLutView === undefined
806
857
  ) {
807
- return err(badAlloc('ibl-cache-resources'));
858
+ return fail('ibl-cache-resources');
808
859
  }
809
860
 
810
861
  // Bind group: equirect group(1).
@@ -816,7 +867,7 @@ export function runIblPrecompute(
816
867
  { binding: 1, resource: { kind: 'sampler', value: sampler } },
817
868
  ],
818
869
  });
819
- if (!equirectBgResult.ok) return err(badAlloc('equirect-bg', equirectBgResult.error));
870
+ if (!equirectBgResult.ok) return fail('equirect-bg', equirectBgResult.error);
820
871
  const equirectBg: BindGroup = equirectBgResult.value;
821
872
 
822
873
  // Bind group: cube group(1) for irradiance + prefilter.
@@ -828,14 +879,14 @@ export function runIblPrecompute(
828
879
  { binding: 1, resource: { kind: 'sampler', value: sampler } },
829
880
  ],
830
881
  });
831
- if (!cubeBgResult.ok) return err(badAlloc('cube-bg', cubeBgResult.error));
882
+ if (!cubeBgResult.ok) return fail('cube-bg', cubeBgResult.error);
832
883
  const cubeBg: BindGroup = cubeBgResult.value;
833
884
 
834
885
  // (a) equirect-to-cube: 6 face draws.
835
886
  const cubeFaceViews = opts.cubeFaceViews;
836
887
  for (let face = 0; face < 6; face++) {
837
888
  const cubeFaceView = cubeFaceViews[face];
838
- if (cubeFaceView === undefined) return err(badAlloc('cube-face-view'));
889
+ if (cubeFaceView === undefined) return fail('cube-face-view');
839
890
  const pass: RhiRenderPassEncoder = encoder.beginRenderPass({
840
891
  label: 'ibl-equirect-to-cube',
841
892
  colorAttachments: [
@@ -865,7 +916,7 @@ export function runIblPrecompute(
865
916
  },
866
917
  ],
867
918
  });
868
- if (!faceBgResult.ok) return err(badAlloc('face-bg', faceBgResult.error));
919
+ if (!faceBgResult.ok) return fail('face-bg', faceBgResult.error);
869
920
  pass.setPipeline(equirectPipeline);
870
921
  pass.setBindGroup(0, faceBgResult.value);
871
922
  pass.setBindGroup(1, equirectBg);
@@ -874,10 +925,13 @@ export function runIblPrecompute(
874
925
  pass.end();
875
926
  }
876
927
 
928
+ const cubeStage = await submitStage('equirect-to-cube', true);
929
+ if (!cubeStage.ok) return err(cubeStage.error);
930
+
877
931
  // (b) irradiance convolve: 6 face draws.
878
932
  for (let face = 0; face < 6; face++) {
879
933
  const irrFaceView = irrFaceViews[face];
880
- if (irrFaceView === undefined) return err(badAlloc('irradiance-face-view'));
934
+ if (irrFaceView === undefined) return fail('irradiance-face-view');
881
935
  const pass: RhiRenderPassEncoder = encoder.beginRenderPass({
882
936
  label: 'ibl-irradiance',
883
937
  colorAttachments: [
@@ -906,23 +960,26 @@ export function runIblPrecompute(
906
960
  },
907
961
  ],
908
962
  });
909
- if (!faceBgResult.ok) return err(badAlloc('irr-face-bg', faceBgResult.error));
963
+ if (!faceBgResult.ok) return fail('irr-face-bg', faceBgResult.error);
910
964
  pass.setPipeline(irradiancePipeline);
911
965
  pass.setBindGroup(0, faceBgResult.value);
912
966
  pass.setBindGroup(1, cubeBg);
913
967
  pass.setVertexBuffer(0, opts.cubeVertexBuffer);
914
968
  pass.draw(6, 1, face * 6, 0);
915
969
  pass.end();
970
+
971
+ const irradianceFaceStage = await submitStage(`irradiance-face-${face}`, true);
972
+ if (!irradianceFaceStage.ok) return err(irradianceFaceStage.error);
916
973
  }
917
974
 
918
975
  // (c) prefilter env: 5 mips x 6 faces = 30 sub-passes.
919
976
  for (let mip = 0; mip < PREFILTER_MIP_LEVELS; mip++) {
920
977
  const mipFaceViews = prefMipViews[mip];
921
- if (mipFaceViews === undefined) return err(badAlloc('prefilter-mip-views'));
978
+ if (mipFaceViews === undefined) return fail('prefilter-mip-views');
922
979
  for (let face = 0; face < 6; face++) {
923
980
  const subIdx = mip * 6 + face;
924
981
  const mipFaceView = mipFaceViews[face];
925
- if (mipFaceView === undefined) return err(badAlloc('prefilter-face-view'));
982
+ if (mipFaceView === undefined) return fail('prefilter-face-view');
926
983
  const pass: RhiRenderPassEncoder = encoder.beginRenderPass({
927
984
  label: 'ibl-prefilter',
928
985
  colorAttachments: [
@@ -962,7 +1019,7 @@ export function runIblPrecompute(
962
1019
  },
963
1020
  ],
964
1021
  });
965
- if (!bgResult.ok) return err(badAlloc('pref-bg', bgResult.error));
1022
+ if (!bgResult.ok) return fail('pref-bg', bgResult.error);
966
1023
  pass.setPipeline(prefilterPipeline);
967
1024
  pass.setBindGroup(0, bgResult.value);
968
1025
  pass.setBindGroup(1, cubeBg);
@@ -972,6 +1029,9 @@ export function runIblPrecompute(
972
1029
  }
973
1030
  }
974
1031
 
1032
+ const prefilterStage = await submitStage('prefilter', true);
1033
+ if (!prefilterStage.ok) return err(prefilterStage.error);
1034
+
975
1035
  // (d) brdf-lut: fullscreen triangle.
976
1036
  {
977
1037
  const pass: RhiRenderPassEncoder = encoder.beginRenderPass({
@@ -990,13 +1050,30 @@ export function runIblPrecompute(
990
1050
  pass.end();
991
1051
  }
992
1052
 
993
- // Finish + submit. Counter increments are STRICTLY after submit; if
994
- // submit throws or fails, counters stay at 0 (AC-20 critical invariant).
995
- const finishRes = encoder.finish();
996
- if (!finishRes.ok) return err(badAlloc('finish', finishRes.error));
997
- const cmdBuffer: CommandBuffer = finishRes.value;
998
- const submitRes = device.queue.submit([cmdBuffer]);
999
- if (!submitRes.ok) return err(badAlloc('queue.submit', submitRes.error));
1053
+ // The final stage is fenced before the candidate is promoted.
1054
+ const brdfStage = await submitStage('brdf-lut', false);
1055
+ if (!brdfStage.ok) return err(brdfStage.error);
1056
+
1057
+ const promoted: IblPrecomputeCandidate = {
1058
+ irradianceTexture: candidateIrradianceTexture,
1059
+ irradianceView: candidateIrradianceView,
1060
+ irradianceFaceViews: irrFaceViews,
1061
+ prefilterTexture: candidatePrefilterTexture,
1062
+ prefilterView: candidatePrefilterView,
1063
+ prefilterFaceViewsByMip: prefMipViews,
1064
+ brdfLutTexture: candidateBrdfLutTexture,
1065
+ brdfLutView,
1066
+ };
1067
+ // Previous outputs remain DeviceScope-owned until scope retirement. This
1068
+ // avoids destroying a texture still referenced by an already-recorded pass.
1069
+ cache.irradianceTexture = promoted.irradianceTexture;
1070
+ cache.irradianceView = promoted.irradianceView;
1071
+ cache.irradianceFaceViews = promoted.irradianceFaceViews;
1072
+ cache.prefilterTexture = promoted.prefilterTexture;
1073
+ cache.prefilterView = promoted.prefilterView;
1074
+ cache.prefilterFaceViewsByMip = promoted.prefilterFaceViewsByMip;
1075
+ cache.brdfLutTexture = promoted.brdfLutTexture;
1076
+ cache.brdfLutView = promoted.brdfLutView;
1000
1077
 
1001
1078
  // POST-SUBMIT counter increments (AC-20).
1002
1079
  cache.irradianceBakeCount += 1;
@@ -1006,6 +1083,13 @@ export function runIblPrecompute(
1006
1083
  return ok({ submitted: true });
1007
1084
  }
1008
1085
 
1086
+ function adoptIblTexture(scope: DeviceScope, device: IblRhiOwner, texture: Texture): Texture {
1087
+ scope._adopt('texture', texture, (value) => {
1088
+ device.destroyTexture(value);
1089
+ });
1090
+ return texture;
1091
+ }
1092
+
1009
1093
  // Local helper -- structured error payload shared across allocation paths.
1010
1094
  function badAlloc(stage: string, cause?: RhiError): IblPrecomputeError {
1011
1095
  return {
@@ -240,6 +240,13 @@ export interface TransmissionBindGroupResources {
240
240
  readonly backdropView?: TextureView | null;
241
241
  }
242
242
 
243
+ /** Engine-owned texture injection resources derived from material parameters. */
244
+ export interface TextureInjectionResource {
245
+ readonly slot: number;
246
+ readonly sampler: Sampler;
247
+ readonly view: TextureView;
248
+ }
249
+
243
250
  // Lightmap (emissive + occlusion sampler/texture pair x 2) and transmission
244
251
  // injection starts are computed from the assembled list length on each push —
245
252
  // no hardcoded binding literals.
@@ -248,6 +255,7 @@ export function assembleMaterialWithSkylightEntries(
248
255
  materialEntries: readonly BindGroupEntry[],
249
256
  skylight: SkylightBindGroupResources,
250
257
  transmission?: TransmissionBindGroupResources | undefined,
258
+ textureInjections: readonly TextureInjectionResource[] = [],
251
259
  ): BindGroupEntry[] {
252
260
  // IBL injection start = end of the user-region. The user-region IS
253
261
  // `materialEntries` (UBO binding 0 + N sampler/texture pairs), so its length
@@ -303,6 +311,24 @@ export function assembleMaterialWithSkylightEntries(
303
311
  resource: { kind: 'textureView', value: transmissionView },
304
312
  },
305
313
  );
314
+ const clearcoatStart = result.length;
315
+ for (let index = 0; index < textureInjections.length; index += 1) {
316
+ const resource = textureInjections[index];
317
+ if (resource === undefined) continue;
318
+ // Slots are offsets from one stable injection base. Using the growing
319
+ // result length here made slot 1/2 drift past their BGL bindings.
320
+ const binding = clearcoatStart + resource.slot * 2;
321
+ result.push(
322
+ {
323
+ binding,
324
+ resource: { kind: 'sampler', value: resource.sampler },
325
+ },
326
+ {
327
+ binding: binding + 1,
328
+ resource: { kind: 'textureView', value: resource.view },
329
+ },
330
+ );
331
+ }
306
332
  return result;
307
333
  }
308
334
 
package/src/index.ts CHANGED
@@ -196,7 +196,15 @@ export type {
196
196
  ReflectionProbeSelectionInspection,
197
197
  TransmissionInspection,
198
198
  } from './inspection-types';
199
- export { Materials, MaterialTransmissionContractError } from './materials';
199
+ export {
200
+ type MaterialColorInput3,
201
+ type MaterialColorInput4,
202
+ type MaterialColorTuple3,
203
+ type MaterialColorTuple4,
204
+ Materials,
205
+ MaterialTransmissionContractError,
206
+ srgb,
207
+ } from './materials';
200
208
  export {
201
209
  type MeshMaterialBindingObservation,
202
210
  type MeshMaterialBindingPreparationFailure,
@@ -46,6 +46,9 @@ export const STORAGE_BUFFER_MIN_REQUIRED = 4;
46
46
 
47
47
  /** Byte size of one Point/Spot/Rect DirectLightSlot. */
48
48
  export const BYTES_PER_DIRECT_LIGHT_SLOT = 80;
49
+ /** std430 slot aliases retained for the WebGPU ready-state sizing table. */
50
+ export const POINT_LIGHT_STD430_BYTES = BYTES_PER_DIRECT_LIGHT_SLOT;
51
+ export const SPOT_LIGHT_STD430_BYTES = BYTES_PER_DIRECT_LIGHT_SLOT;
49
52
 
50
53
  /**
51
54
  * Cap gate at createRenderer time: check whether the device has enough