@forgeax/engine-render 0.1.23 → 0.1.25

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 (322) hide show
  1. package/README.md +231 -10
  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/surface-projection.d.ts +24 -0
  15. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-policy.d.ts +188 -0
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  18. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  19. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  20. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  21. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  23. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  24. package/dist/assembly/recovery/generation.d.ts +46 -0
  25. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  26. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  27. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  28. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  29. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  30. package/dist/assembly/renderer-facade.d.ts +10 -0
  31. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +19 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  36. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  37. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  41. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-ready.d.ts +55 -0
  43. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  45. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  47. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  48. package/dist/assets/asset-decoders.d.ts.map +1 -1
  49. package/dist/authoring.mjs +1 -1
  50. package/dist/{chunk-FF7K3FSO.mjs → chunk-ADHHVYLW.mjs} +89 -89
  51. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  52. package/dist/{chunk-YALPS44A.mjs → chunk-E34VL5VI.mjs} +47 -47
  53. package/dist/chunk-E34VL5VI.mjs.map +1 -0
  54. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  55. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  56. package/dist/{chunk-HZUPSYW7.mjs → chunk-HKXTW355.mjs} +105 -15
  57. package/dist/chunk-HKXTW355.mjs.map +1 -0
  58. package/dist/chunk-JSEDTZYS.mjs +413 -0
  59. package/dist/chunk-JSEDTZYS.mjs.map +1 -0
  60. package/dist/{chunk-FBC4R44J.mjs → chunk-KM2NOX2I.mjs} +57 -57
  61. package/dist/chunk-KM2NOX2I.mjs.map +1 -0
  62. package/dist/{chunk-DIYW3M5D.mjs → chunk-N3RT2EUR.mjs} +2288 -2238
  63. package/dist/chunk-N3RT2EUR.mjs.map +1 -0
  64. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-TCLDX7SZ.mjs} +1167 -368
  65. package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
  66. package/dist/{chunk-QODA5UKU.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  67. package/dist/chunk-ZZ4YQ474.mjs.map +1 -0
  68. package/dist/components/instances.d.ts +13 -31
  69. package/dist/components/instances.d.ts.map +1 -1
  70. package/dist/construct-renderer.mjs +20688 -16314
  71. package/dist/construct-renderer.mjs.map +1 -1
  72. package/dist/device/device-scope.d.ts +2 -0
  73. package/dist/device/device-scope.d.ts.map +1 -1
  74. package/dist/device/gpu-residency.d.ts +24 -4
  75. package/dist/device/gpu-residency.d.ts.map +1 -1
  76. package/dist/environment/frame.d.ts +5 -0
  77. package/dist/environment/frame.d.ts.map +1 -1
  78. package/dist/environment/inspection.d.ts +7 -0
  79. package/dist/environment/inspection.d.ts.map +1 -1
  80. package/dist/environment/lifecycle.d.ts +20 -2
  81. package/dist/environment/lifecycle.d.ts.map +1 -1
  82. package/dist/errors/recover.d.ts +58 -2
  83. package/dist/errors/recover.d.ts.map +1 -1
  84. package/dist/errors/render.d.ts +13 -0
  85. package/dist/errors/render.d.ts.map +1 -1
  86. package/dist/extract/contracts.d.ts +899 -0
  87. package/dist/extract/contracts.d.ts.map +1 -0
  88. package/dist/features/host.d.ts +2 -0
  89. package/dist/features/host.d.ts.map +1 -1
  90. package/dist/features/prepared-gpu-work.d.ts +3 -0
  91. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  92. package/dist/features/prepared-graphics-store.d.ts +2 -0
  93. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  94. package/dist/gpu-driven/production-raster.d.ts +34 -1
  95. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +5 -0
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  99. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  100. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  101. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  102. package/dist/index.d.ts +2 -1
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.mjs +27 -9
  105. package/dist/index.mjs.map +1 -1
  106. package/dist/instance-buffer-cache.d.ts +7 -1
  107. package/dist/instance-buffer-cache.d.ts.map +1 -1
  108. package/dist/instances.d.ts +101 -0
  109. package/dist/instances.d.ts.map +1 -0
  110. package/dist/internal.mjs +6 -6
  111. package/dist/light-buffer-layout.d.ts +3 -0
  112. package/dist/light-buffer-layout.d.ts.map +1 -1
  113. package/dist/materials.d.ts +96 -11
  114. package/dist/materials.d.ts.map +1 -1
  115. package/dist/mesh-material-bindings.d.ts +56 -0
  116. package/dist/mesh-material-bindings.d.ts.map +1 -1
  117. package/dist/occlusion-proxy.d.ts +4 -0
  118. package/dist/occlusion-proxy.d.ts.map +1 -0
  119. package/dist/pbr-pipeline.d.ts +32 -9
  120. package/dist/pbr-pipeline.d.ts.map +1 -1
  121. package/dist/pipeline-builder.d.ts.map +1 -1
  122. package/dist/pipeline-spec.d.ts.map +1 -1
  123. package/dist/points-lines/prepare.d.ts +3 -0
  124. package/dist/points-lines/prepare.d.ts.map +1 -1
  125. package/dist/points-lines/standard-owner.d.ts +33 -0
  126. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  127. package/dist/record/frame-snapshot.d.ts +18 -2
  128. package/dist/record/frame-snapshot.d.ts.map +1 -1
  129. package/dist/record/frame-targets.d.ts.map +1 -1
  130. package/dist/record/frame.d.ts +14 -2
  131. package/dist/record/frame.d.ts.map +1 -1
  132. package/dist/record/main-pass-geometry.d.ts +36 -4
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +7 -17
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass.d.ts.map +1 -1
  137. package/dist/record/mesh-ssbo.d.ts +11 -3
  138. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  139. package/dist/record/recovery-pipeline.d.ts +30 -0
  140. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  141. package/dist/record/render-context.d.ts +28 -5
  142. package/dist/record/render-context.d.ts.map +1 -1
  143. package/dist/record/shadow-pass.d.ts +11 -1
  144. package/dist/record/shadow-pass.d.ts.map +1 -1
  145. package/dist/record/typed-frame-graph.d.ts +1 -1
  146. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  147. package/dist/recovery/render-system-candidate.d.ts +65 -0
  148. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  149. package/dist/render-contract.d.ts +68 -7
  150. package/dist/render-contract.d.ts.map +1 -1
  151. package/dist/render-system-extract-tail.d.ts +16 -0
  152. package/dist/render-system-extract-tail.d.ts.map +1 -0
  153. package/dist/render-system-extract.d.ts +180 -22
  154. package/dist/render-system-extract.d.ts.map +1 -1
  155. package/dist/render-system.d.ts +131 -4
  156. package/dist/render-system.d.ts.map +1 -1
  157. package/dist/scene/render-scene-types.d.ts +2 -0
  158. package/dist/scene/render-scene-types.d.ts.map +1 -1
  159. package/dist/scene/render-scene.d.ts +31 -1
  160. package/dist/scene/render-scene.d.ts.map +1 -1
  161. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  162. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  163. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  164. package/dist/temporal/index.mjs +6 -6
  165. package/dist/temporal/taa-history-store.d.ts +15 -1
  166. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  167. package/dist/temporal/transaction.d.ts +5 -0
  168. package/dist/temporal/transaction.d.ts.map +1 -1
  169. package/package.json +20 -20
  170. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  171. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  172. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  173. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  174. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  175. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  176. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  177. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  178. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  179. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  180. package/src/__tests__/instances-store.unit.test.ts +46 -0
  181. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  182. package/src/__tests__/material-contract-inventory.unit.test.ts +29 -22
  183. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  184. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  185. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  186. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  187. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  188. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  189. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  190. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  191. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -5
  192. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  193. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  194. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  195. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  196. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  197. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +127 -18
  198. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  199. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  200. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  201. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  202. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  203. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  204. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  205. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  206. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  207. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  208. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  209. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  210. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  211. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  212. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  213. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  214. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  215. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  216. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  217. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  218. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  219. package/src/assembly/bundler-contract.ts +9 -0
  220. package/src/assembly/device-feature-admission.ts +15 -5
  221. package/src/assembly/extended-lighting-fallback.ts +216 -0
  222. package/src/assembly/factory.ts +83 -10088
  223. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  224. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  225. package/src/assembly/material/assembly.ts +18 -2
  226. package/src/assembly/material/standard-layer-projection.ts +16 -0
  227. package/src/assembly/material/surface-projection.ts +90 -0
  228. package/src/assembly/material-shader-policy.ts +932 -0
  229. package/src/assembly/material-shader-prewarm.ts +88 -0
  230. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  231. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  232. package/src/assembly/recovery/generation.ts +141 -0
  233. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  234. package/src/assembly/recovery/renderer-recover.ts +906 -0
  235. package/src/assembly/renderer-facade.ts +342 -0
  236. package/src/assembly/renderer-frame-transaction.ts +45 -0
  237. package/src/assembly/renderer-helpers.ts +99 -0
  238. package/src/assembly/renderer-lifecycle.ts +159 -0
  239. package/src/assembly/shader-prewarm-policy.ts +157 -0
  240. package/src/assembly/webgpu-ready-contract.ts +14 -0
  241. package/src/assembly/webgpu-ready.ts +4066 -0
  242. package/src/assembly/webgpu-renderer.ts +4027 -0
  243. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  244. package/src/assets/asset-decoders.ts +27 -4
  245. package/src/components/camera.ts +1 -1
  246. package/src/components/instances.ts +14 -197
  247. package/src/debug-draw-glue.ts +1 -1
  248. package/src/device/device-scope.ts +35 -6
  249. package/src/device/gpu-residency.ts +194 -26
  250. package/src/environment/frame.ts +6 -0
  251. package/src/environment/inspection.ts +10 -0
  252. package/src/environment/lifecycle.ts +114 -8
  253. package/src/errors/recover.ts +76 -5
  254. package/src/errors/render.ts +17 -0
  255. package/src/extract/contracts.ts +974 -0
  256. package/src/features/host.ts +16 -0
  257. package/src/features/prepared-gpu-work.ts +20 -2
  258. package/src/features/prepared-graphics-store.ts +17 -0
  259. package/src/gpu-driven/production-raster.ts +131 -3
  260. package/src/gpu-scene.ts +87 -31
  261. package/src/ibl/IblPipelineCache.ts +134 -50
  262. package/src/ibl/skylight-bind-group.ts +26 -0
  263. package/src/index.ts +10 -1
  264. package/src/instance-buffer-cache.ts +14 -1
  265. package/src/instances.ts +288 -0
  266. package/src/light-buffer-layout.ts +3 -0
  267. package/src/materials.ts +365 -53
  268. package/src/mesh-material-bindings.ts +59 -0
  269. package/src/occlusion-proxy.ts +64 -0
  270. package/src/pbr-pipeline.ts +139 -15
  271. package/src/pipeline-builder.ts +14 -10
  272. package/src/pipeline-spec.ts +6 -14
  273. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  274. package/src/points-lines/prepare.ts +9 -0
  275. package/src/points-lines/standard-owner.ts +403 -0
  276. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  277. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  278. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  279. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  280. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  281. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  282. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  283. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  284. package/src/record/frame-snapshot.ts +34 -2
  285. package/src/record/frame-targets.ts +11 -1
  286. package/src/record/frame.ts +37 -4
  287. package/src/record/helpers.ts +1 -1
  288. package/src/record/main-pass-geometry.ts +727 -63
  289. package/src/record/main-pass-material.ts +316 -209
  290. package/src/record/main-pass-sprite-draws.ts +7 -8
  291. package/src/record/main-pass.ts +94 -6
  292. package/src/record/mesh-ssbo.ts +32 -10
  293. package/src/record/recovery-pipeline.ts +695 -0
  294. package/src/record/render-context.ts +39 -4
  295. package/src/record/shadow-pass.ts +46 -99
  296. package/src/record/typed-frame-graph.ts +45 -1
  297. package/src/record/view-ubo.ts +3 -3
  298. package/src/recovery/render-system-candidate.ts +876 -0
  299. package/src/render-contract.ts +69 -7
  300. package/src/render-system-extract-tail.ts +3039 -0
  301. package/src/render-system-extract.ts +177 -3003
  302. package/src/render-system.ts +906 -662
  303. package/src/scene/render-scene-types.ts +2 -0
  304. package/src/scene/render-scene.ts +703 -37
  305. package/src/systems/skin-palette-allocator.ts +129 -7
  306. package/src/temporal/frame-coordinator.ts +4 -1
  307. package/src/temporal/taa-history-store.ts +46 -2
  308. package/src/temporal/temporal-view.ts +1 -1
  309. package/src/temporal/transaction.ts +9 -0
  310. package/src/tilemap-chunk-extract-system.ts +1 -1
  311. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  312. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  313. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  314. package/dist/chunk-4NPUASZR.mjs +0 -203
  315. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  316. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  317. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  318. package/dist/chunk-FF7K3FSO.mjs.map +0 -1
  319. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  320. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  321. package/dist/chunk-YALPS44A.mjs.map +0 -1
  322. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import type { EntityHandle } from '@forgeax/engine-ecs';
2
2
  import type { RhiCaps, TextureFormat } from '@forgeax/engine-rhi';
3
3
  import { err, ok, type Result } from '@forgeax/engine-types';
4
+ import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
4
5
  import {
5
6
  type RenderError,
6
7
  RenderFeatureCapabilityMissingError,
@@ -175,6 +176,7 @@ export interface RenderFeatureHost {
175
176
  batches: readonly RenderFeaturePreparedResourceBatch[],
176
177
  ): Result<void, RenderError>;
177
178
  recover(input: RenderFeatureRecoverInput): Result<void, RenderError>;
179
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown>;
178
180
  diagnostics(): readonly RenderFeatureDiagnostics[];
179
181
  /** Subscribe to lifecycle projection changes; unchanged active frames are silent. */
180
182
  subscribeDiagnostics(listener: () => void): () => void;
@@ -915,6 +917,20 @@ class FeatureHostImpl implements RenderFeatureHost {
915
917
  return this.slots.map((slot) => slot.feature);
916
918
  }
917
919
 
920
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
921
+ return {
922
+ kind: 'feature',
923
+ create: () => {
924
+ if (!scope.isAlive()) throw new Error('Feature candidate scope is not active.');
925
+ // The root carries the detached host owner itself. A scalar marker is
926
+ // not evidence that feature planning/preparation actually ran; the
927
+ // candidate graph and host own the prepared resources.
928
+ return this;
929
+ },
930
+ cleanup: () => undefined,
931
+ };
932
+ }
933
+
918
934
  private publishDiagnosticsChanged(): void {
919
935
  for (const listener of this.diagnosticsListeners) {
920
936
  try {
@@ -11,6 +11,7 @@ import type {
11
11
  ShaderModule,
12
12
  } from '@forgeax/engine-rhi';
13
13
  import { type BindGroupLayoutDescriptor, err, ok, type Result } from '@forgeax/engine-types';
14
+ import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
14
15
  import { type RenderError, RenderFeaturePreparationFailedError } from '../errors/render';
15
16
  import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
16
17
  import type { RenderFeatureShaderModuleMode } from './types';
@@ -910,6 +911,8 @@ function createRenderFeatureGpuWorkSession(input: {
910
911
  * retains the per-feature prepared GPU state and recreates it after recovery.
911
912
  */
912
913
  export interface RenderFeatureGpuWorkOwner {
914
+ /** Candidate root carries this detached GPU-work owner itself. */
915
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown>;
913
916
  beginFeature(
914
917
  featureIdentity: string,
915
918
  generation: number,
@@ -935,9 +938,21 @@ export function createRenderFeatureGpuWorkOwner(input: {
935
938
  session: RenderFeatureGpuPrepareSession;
936
939
  }
937
940
  >();
938
-
939
- return {
941
+ let disposed = false;
942
+
943
+ const owner: RenderFeatureGpuWorkOwner = {
944
+ createRecoveryRoot: (scope) => ({
945
+ kind: 'feature',
946
+ create: () => {
947
+ if (!scope.isAlive()) throw new Error('Feature GPU candidate scope is not active.');
948
+ return owner;
949
+ },
950
+ cleanup: () => undefined,
951
+ }),
940
952
  beginFeature: (featureIdentity, generation, requestedShaderModuleMode = 'validated') => {
953
+ if (disposed) {
954
+ throw new Error('Render feature GPU work owner is disposed.');
955
+ }
941
956
  const shaderModuleMode = requestedShaderModuleMode;
942
957
  const existing = sessions.get(featureIdentity);
943
958
  if (existing?.generation === generation && existing.shaderModuleMode === shaderModuleMode) {
@@ -962,6 +977,8 @@ export function createRenderFeatureGpuWorkOwner(input: {
962
977
  resolveBuffer: (featureIdentity, reference) =>
963
978
  sessions.get(featureIdentity)?.session.resolveBuffer(reference),
964
979
  dispose: () => {
980
+ if (disposed) return ok(undefined);
981
+ disposed = true;
965
982
  let first: RenderError | undefined;
966
983
  for (const { session } of sessions.values()) {
967
984
  const disposed = session.dispose();
@@ -971,4 +988,5 @@ export function createRenderFeatureGpuWorkOwner(input: {
971
988
  return first === undefined ? ok(undefined) : err(first);
972
989
  },
973
990
  };
991
+ return owner;
974
992
  }
@@ -1,5 +1,6 @@
1
1
  import type { TextureFormat } from '@forgeax/engine-rhi';
2
2
  import { err, ok, type Result } from '@forgeax/engine-types';
3
+ import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
3
4
  import { type RenderError, RenderFeaturePreparationFailedError } from '../errors/render';
4
5
  import type {
5
6
  PreparedKind,
@@ -81,6 +82,7 @@ export interface PreparedGraphicsStore {
81
82
  beginFrame(featureIdentity: string, generation: number): PreparedGraphicsTransaction;
82
83
  snapshot(featureIdentity: string): PreparedGraphicsStoreSnapshot;
83
84
  invalidate(featureIdentity: string, generation: number): void;
85
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown>;
84
86
  }
85
87
 
86
88
  interface Slot {
@@ -418,6 +420,21 @@ class PreparedGraphicsStoreImpl implements PreparedGraphicsStore {
418
420
  }
419
421
  }
420
422
 
423
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
424
+ return {
425
+ kind: 'buffer',
426
+ create: () => {
427
+ if (!scope.isAlive()) throw new Error('Prepared graphics candidate scope is not active.');
428
+ const itemCount = [...this.slots.values()].reduce(
429
+ (count, slot) => count + slot.items.size,
430
+ 0,
431
+ );
432
+ return Object.freeze({ generation: scope.generation, itemCount });
433
+ },
434
+ cleanup: () => undefined,
435
+ };
436
+ }
437
+
421
438
  registerReference(reference: object, item: PreparedGraphicsItem): void {
422
439
  this.references.set(reference, item);
423
440
  }
@@ -20,6 +20,7 @@ import type {
20
20
  import { err, ok, RhiError } from '@forgeax/engine-rhi';
21
21
  import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
22
22
  import type { MaterialRenderState } from '@forgeax/engine-types';
23
+ import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
23
24
  import type { MeshGpuHandles } from '../device/gpu-residency';
24
25
  import { GPU_SCENE_WGSL } from '../gpu-scene-schema';
25
26
  import {
@@ -48,7 +49,12 @@ import {
48
49
  projectedHeightForCandidate,
49
50
  type SubmissionPlan,
50
51
  } from './batch-topology';
51
- import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
52
+ import {
53
+ GPU_DRIVEN_VIEW_WGSL,
54
+ type GpuDrivenLodSelectionInspection,
55
+ GpuDrivenView,
56
+ selectGpuLodLane,
57
+ } from './view-gpu';
52
58
 
53
59
  const VERTEX_STAGE = 0x1;
54
60
  const FRAGMENT_STAGE = 0x2;
@@ -261,8 +267,8 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
261
267
  projection,
262
268
  camera.orthoLeft,
263
269
  camera.orthoRight,
264
- camera.orthoBottom,
265
270
  camera.orthoTop,
271
+ camera.orthoBottom,
266
272
  camera.near,
267
273
  camera.far,
268
274
  );
@@ -297,6 +303,9 @@ function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch)
297
303
  const shader = material.materialShaderId ?? 'forgeax::default-unlit';
298
304
  return (
299
305
  shader === 'forgeax::default-unlit' &&
306
+ // The production rigid shader owns triangle-list raster only. Lines and
307
+ // points stay with their CPU/specialized topology owners.
308
+ batch.key.topology === 'triangle-list' &&
300
309
  batch.key.materialResourceClass === EMPTY_RESOURCE_CLASS &&
301
310
  material.baseColorTexture === undefined &&
302
311
  material.transparent !== true
@@ -928,7 +937,62 @@ export interface PreparedGpuDrivenFrame {
928
937
  ): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
929
938
  }
930
939
 
940
+ export interface RecoveryCapabilityFacts {
941
+ readonly generation: number;
942
+ readonly shaderCompilation: boolean;
943
+ readonly compute: boolean;
944
+ readonly storageBuffer: boolean;
945
+ readonly indirectDrawing: boolean;
946
+ readonly multisample: boolean;
947
+ }
948
+
949
+ export type RecoveryCapabilityOutcome =
950
+ | {
951
+ readonly status: 'ready' | 'fallback';
952
+ readonly generation: number;
953
+ readonly disabled: readonly string[];
954
+ }
955
+ | {
956
+ readonly status: 'disabled';
957
+ readonly generation: number;
958
+ readonly disabled: readonly string[];
959
+ readonly allocations: 0;
960
+ }
961
+ | {
962
+ readonly status: 'refused';
963
+ readonly generation: number;
964
+ readonly failedOwner: 'shader-material-pipeline';
965
+ readonly resourceKind: 'pipeline';
966
+ };
967
+
968
+ export function classifyRecoveryCapability(
969
+ facts: RecoveryCapabilityFacts,
970
+ ): RecoveryCapabilityOutcome {
971
+ if (!facts.shaderCompilation) {
972
+ return {
973
+ status: 'refused',
974
+ generation: facts.generation,
975
+ failedOwner: 'shader-material-pipeline',
976
+ resourceKind: 'pipeline',
977
+ };
978
+ }
979
+ if (!facts.compute || !facts.storageBuffer || !facts.indirectDrawing) {
980
+ return {
981
+ status: 'disabled',
982
+ generation: facts.generation,
983
+ disabled: ['gpu-driven'],
984
+ allocations: 0,
985
+ };
986
+ }
987
+ return {
988
+ status: facts.multisample ? 'ready' : 'fallback',
989
+ generation: facts.generation,
990
+ disabled: [],
991
+ };
992
+ }
993
+
931
994
  export class GpuDrivenProduction {
995
+ private disposed = false;
932
996
  private view: GpuDrivenView | undefined;
933
997
  private raster: GpuDrivenRigidRaster | undefined;
934
998
  private standardRaster: StandardPbrRasterAdapter | undefined;
@@ -972,6 +1036,55 @@ export class GpuDrivenProduction {
972
1036
  private readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory,
973
1037
  ) {}
974
1038
 
1039
+ static forDevice(
1040
+ device: RhiDevice,
1041
+ shaderModuleFactory: PipelineBuilderShaderModuleFactory,
1042
+ ): GpuDrivenProduction {
1043
+ return new GpuDrivenProduction(device, shaderModuleFactory);
1044
+ }
1045
+
1046
+ /** Recovery has no frame retry: await generated programs before preparing the graph. */
1047
+ static async forRecoveryDevice(
1048
+ device: RhiDevice,
1049
+ factory: PipelineBuilderShaderModuleFactory,
1050
+ createShaderModule: (descriptor: {
1051
+ code: string;
1052
+ label?: string;
1053
+ }) => Promise<Result<ShaderModule, RhiError>>,
1054
+ ): Promise<Result<GpuDrivenProduction, RhiError>> {
1055
+ const modules = new Map<string, ShaderModule>();
1056
+ for (const descriptor of [
1057
+ { label: 'gpu-driven-view', code: GPU_DRIVEN_VIEW_WGSL },
1058
+ { label: 'gpu-driven-rigid-unlit', code: GPU_DRIVEN_RIGID_UNLIT_WGSL },
1059
+ ]) {
1060
+ const module = await createShaderModule(descriptor);
1061
+ if (!module.ok) return module;
1062
+ modules.set(descriptor.code, module.value);
1063
+ }
1064
+ return ok(
1065
+ new GpuDrivenProduction(device, {
1066
+ createShaderModule: (descriptor) => {
1067
+ const module = modules.get(descriptor.code);
1068
+ return module === undefined ? factory.createShaderModule(descriptor) : ok(module);
1069
+ },
1070
+ }),
1071
+ );
1072
+ }
1073
+
1074
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
1075
+ return {
1076
+ kind: 'scene-table',
1077
+ create: () => {
1078
+ if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
1079
+ // Keep the actual detached production owner in the generation
1080
+ // aggregate. The recovery transaction, not a marker object, is the
1081
+ // owner that is published or discarded exactly once.
1082
+ return this;
1083
+ },
1084
+ cleanup: () => undefined,
1085
+ };
1086
+ }
1087
+
975
1088
  prepare(input: {
976
1089
  readonly scene: PersistentGpuDrivenState | undefined;
977
1090
  readonly camera: CameraSnapshot;
@@ -1000,7 +1113,20 @@ export class GpuDrivenProduction {
1000
1113
  this.telemetryGeometryWork = 0;
1001
1114
  this.telemetryRootGeometryWork = 0;
1002
1115
  this.indirectDrawCount = 0;
1003
- if (input.hdrp || input.scene === undefined || selectGpuLodLane(this.device.caps) === 'cpu') {
1116
+ const capability = classifyRecoveryCapability({
1117
+ generation: 0,
1118
+ shaderCompilation: true,
1119
+ compute: this.device.caps.compute,
1120
+ storageBuffer: this.device.caps.storageBuffer,
1121
+ indirectDrawing: this.device.caps.indirectDrawing,
1122
+ multisample: true,
1123
+ });
1124
+ if (
1125
+ input.hdrp ||
1126
+ input.scene === undefined ||
1127
+ capability.status === 'disabled' ||
1128
+ selectGpuLodLane(this.device.caps) === 'cpu'
1129
+ ) {
1004
1130
  this.gpuOwnedEntityCount = 0;
1005
1131
  this.sceneRowsRequired = false;
1006
1132
  return ok(undefined);
@@ -1457,6 +1583,8 @@ export class GpuDrivenProduction {
1457
1583
  }
1458
1584
 
1459
1585
  dispose(): void {
1586
+ if (this.disposed) return;
1587
+ this.disposed = true;
1460
1588
  this.view?.dispose();
1461
1589
  this.view = undefined;
1462
1590
  this.raster = undefined;
package/src/gpu-scene.ts CHANGED
@@ -201,6 +201,7 @@ function coalesceSlots(
201
201
 
202
202
  /** Persistent GPU tables derived only from renderer projection slots. */
203
203
  export class GpuScene {
204
+ private disposed = false;
204
205
  private tableBytes: Record<GpuSceneTableName, ArrayBuffer>;
205
206
  private transformBytes: Uint8Array;
206
207
  private transformView: DataView;
@@ -224,6 +225,10 @@ export class GpuScene {
224
225
  drawTemplate: [],
225
226
  material: [],
226
227
  };
228
+ /** Transform rows whose previous value advances only after submit. */
229
+ private readonly pendingTemporalTransforms = new Set<number>();
230
+ /** Previous rows whose post-submit upload must be retried before the next draw. */
231
+ private readonly pendingTemporalUploads = new Set<number>();
227
232
 
228
233
  private constructor(
229
234
  private readonly device: RhiDevice,
@@ -272,31 +277,47 @@ export class GpuScene {
272
277
  }
273
278
 
274
279
  sync(delta: RenderSceneApplyResult): Result<GpuSceneSyncResult, RhiError> {
280
+ for (const transformIndex of this.pendingTemporalUploads) {
281
+ this.writesByTable.transform.push(transformIndex);
282
+ }
283
+ const pendingWrites = TABLE_NAMES.some((name) => this.writesByTable[name].length > 0);
275
284
  if (
276
285
  delta.createdSlots.length === 0 &&
277
286
  delta.updatedSlots.length === 0 &&
278
287
  delta.recreatedSlots.length === 0 &&
279
- delta.removedSlots.length === 0
288
+ delta.removedSlots.length === 0 &&
289
+ !pendingWrites
280
290
  ) {
281
291
  this.noChangeFrames += 1;
282
292
  return ok({ ranges: 0, bytes: 0, grew: false, cleared: 0 });
283
293
  }
284
294
  const writesByTable = this.writesByTable;
285
- for (const name of TABLE_NAMES) writesByTable[name].length = 0;
286
- if (!this.identityUploaded) writesByTable.transform.push(0);
295
+ if (!this.identityUploaded && !writesByTable.transform.includes(0)) {
296
+ writesByTable.transform.push(0);
297
+ }
287
298
  let highestChangedSlot = -1;
288
- for (const records of [delta.createdSlots, delta.recreatedSlots]) {
299
+ const contentUpdatedSlots = delta.contentUpdatedSlots ?? [];
300
+ const contentUpdatedSlotIds = new Set(contentUpdatedSlots.map((record) => record.slot));
301
+ const allocationResets = new Set<number>();
302
+ // RenderScene may recycle a released CPU slot for a different identity in
303
+ // the same delta. Clear the old GPU allocation before ensuring the new
304
+ // record; otherwise the later removal pass clears the newly allocated
305
+ // record and the next transform-only update sees no allocation.
306
+ for (const record of delta.removedSlots) {
307
+ if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
308
+ this.clearSlot(record.slot, writesByTable);
309
+ }
310
+ for (const records of [delta.createdSlots, contentUpdatedSlots, delta.recreatedSlots]) {
289
311
  for (const record of records) {
312
+ const previous = this.allocations[record.slot];
290
313
  this.ensureAllocation(record, writesByTable);
314
+ if (previous !== this.allocations[record.slot]) allocationResets.add(record.slot);
291
315
  if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
292
316
  }
293
317
  }
294
318
  for (const record of delta.updatedSlots) {
295
319
  if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
296
320
  }
297
- for (const record of delta.removedSlots) {
298
- if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
299
- }
300
321
  const requiredCapacity = Math.max(
301
322
  highestChangedSlot + 1,
302
323
  this.instances.requiredCapacity(),
@@ -311,11 +332,18 @@ export class GpuScene {
311
332
  }
312
333
 
313
334
  for (const record of delta.createdSlots) this.writeSlot(record, true, writesByTable);
314
- for (const record of delta.updatedSlots) this.writeRootTransform(record, false, writesByTable);
335
+ for (const record of contentUpdatedSlots) {
336
+ this.writeSlot(record, allocationResets.has(record.slot), writesByTable);
337
+ }
338
+ for (const record of delta.updatedSlots) {
339
+ if (contentUpdatedSlotIds.has(record.slot)) continue;
340
+ this.writeRootTransform(record, false, writesByTable);
341
+ }
315
342
  for (const record of delta.recreatedSlots) this.writeSlot(record, true, writesByTable);
316
- for (const record of delta.removedSlots) this.clearSlot(record.slot, writesByTable);
317
343
  const uploaded = this.uploadRows(writesByTable);
318
344
  if (!uploaded.ok) return uploaded;
345
+ this.pendingTemporalUploads.clear();
346
+ for (const name of TABLE_NAMES) writesByTable[name].length = 0;
319
347
  this.identityUploaded = true;
320
348
  this.clearedSlots += delta.removedSlots.length;
321
349
  return ok({
@@ -328,27 +356,47 @@ export class GpuScene {
328
356
 
329
357
  /** Publish current transforms as previous only after a successful submit. */
330
358
  commitTemporalFrame(enabled = true): Result<void, RhiError> {
331
- if (!enabled) return ok(undefined);
332
- const nextTransform = this.tableBytes.transform.slice(0);
333
- const next = new Uint8Array(nextTransform);
334
- const current = this.transformBytes;
359
+ if (!enabled) {
360
+ this.pendingTemporalTransforms.clear();
361
+ this.pendingTemporalUploads.clear();
362
+ return ok(undefined);
363
+ }
364
+ if (this.pendingTemporalTransforms.size === 0) return ok(undefined);
335
365
  const currentOffset = offset(TRANSFORM, 'currentWorld');
336
366
  const previousOffset = offset(TRANSFORM, 'previousWorld');
337
- for (let index = 0; index < this.capacity; index += 1) {
338
- next.set(
339
- current.subarray(
367
+ const ranges = coalesceSlots([...this.pendingTemporalTransforms]);
368
+ for (const range of ranges) {
369
+ for (let index = range.start; index < range.end; index += 1) {
370
+ this.pendingTemporalUploads.add(index);
371
+ }
372
+ }
373
+ for (const range of ranges) {
374
+ for (let index = range.start; index < range.end; index += 1) {
375
+ this.transformBytes.copyWithin(
376
+ index * TRANSFORM.stride + previousOffset,
340
377
  index * TRANSFORM.stride + currentOffset,
341
378
  index * TRANSFORM.stride + currentOffset + 64,
342
- ),
343
- index * TRANSFORM.stride + previousOffset,
379
+ );
380
+ }
381
+ }
382
+ for (const range of ranges) {
383
+ const firstOffset = range.start * TRANSFORM.stride;
384
+ const lastOffset = range.end * TRANSFORM.stride;
385
+ const uploaded = this.device.queue.writeBuffer(
386
+ this.buffers.transform,
387
+ firstOffset,
388
+ this.transformBytes.subarray(firstOffset, lastOffset),
344
389
  );
390
+ if (!uploaded.ok) return uploaded;
345
391
  }
346
- const uploaded = this.device.queue.writeBuffer(this.buffers.transform, 0, next);
347
- if (!uploaded.ok) return uploaded;
348
- this.tableBytes.transform = nextTransform;
349
- this.refreshTransformViews();
350
- this.uploadRanges += 1;
351
- this.uploadBytes += next.byteLength;
392
+ this.pendingTemporalTransforms.clear();
393
+ this.pendingTemporalUploads.clear();
394
+ const bytes = ranges.reduce(
395
+ (total, range) => total + (range.end - range.start) * TRANSFORM.stride,
396
+ 0,
397
+ );
398
+ this.uploadRanges += ranges.length;
399
+ this.uploadBytes += bytes;
352
400
  return ok(undefined);
353
401
  }
354
402
 
@@ -358,6 +406,9 @@ export class GpuScene {
358
406
  this.transforms.reset(1);
359
407
  this.draws.reset();
360
408
  this.materials.reset();
409
+ this.pendingTemporalTransforms.clear();
410
+ this.pendingTemporalUploads.clear();
411
+ for (const name of TABLE_NAMES) this.writesByTable[name].length = 0;
361
412
  const writesByTable: Record<GpuSceneTableName, number[]> = {
362
413
  primitive: [],
363
414
  instance: [],
@@ -432,6 +483,8 @@ export class GpuScene {
432
483
  }
433
484
 
434
485
  dispose(): void {
486
+ if (this.disposed) return;
487
+ this.disposed = true;
435
488
  for (const buffer of Object.values(this.buffers)) this.device.destroyBuffer(buffer);
436
489
  }
437
490
 
@@ -517,7 +570,8 @@ export class GpuScene {
517
570
  const localCurrentOffset = localOffset + offset(TRANSFORM, 'currentWorld');
518
571
  const localPreviousOffset = localOffset + offset(TRANSFORM, 'previousWorld');
519
572
  writeMat4(transform, localCurrentOffset, localWorld);
520
- writeMat4(transform, localPreviousOffset, localWorld);
573
+ if (resetPrevious) writeMat4(transform, localPreviousOffset, localWorld);
574
+ else this.pendingTemporalTransforms.add(transformIndex);
521
575
  writes.transform.push(transformIndex);
522
576
  }
523
577
  writes.instance.push(instanceIndex);
@@ -583,8 +637,6 @@ export class GpuScene {
583
637
  writeVec4(material, materialOffset + offset(MATERIAL, 'params1'), [
584
638
  snapshot.metallic ?? 0,
585
639
  snapshot.roughness ?? 1,
586
- snapshot.clearcoat ?? 0,
587
- snapshot.clearcoatRoughness ?? 0,
588
640
  ]);
589
641
  writeVec4(material, materialOffset + offset(MATERIAL, 'params2'), [
590
642
  snapshot.emissive?.[0] ?? 0,
@@ -630,14 +682,16 @@ export class GpuScene {
630
682
  const allocation = this.allocations[record.slot];
631
683
  if (allocation === undefined) throw new RangeError('GPU Scene allocation unavailable');
632
684
  const transformOffset = allocation.transformStart * TRANSFORM.stride;
633
- const currentOffset = transformOffset + offset(TRANSFORM, 'currentWorld');
634
685
  const previousOffset = transformOffset + offset(TRANSFORM, 'previousWorld');
635
686
  if (resetPrevious) {
636
687
  writeMat4(this.transformView, previousOffset, record.snapshot.transform.world);
637
- } else {
638
- this.transformBytes.copyWithin(previousOffset, currentOffset, currentOffset + 64);
639
688
  }
640
- writeMat4(this.transformView, currentOffset, record.snapshot.transform.world);
689
+ writeMat4(
690
+ this.transformView,
691
+ transformOffset + offset(TRANSFORM, 'currentWorld'),
692
+ record.snapshot.transform.world,
693
+ );
694
+ if (!resetPrevious) this.pendingTemporalTransforms.add(allocation.transformStart);
641
695
  writes.transform.push(allocation.transformStart);
642
696
  }
643
697
 
@@ -742,6 +796,8 @@ export class GpuScene {
742
796
  TRANSFORM.stride,
743
797
  ).fill(0);
744
798
  writes.transform.push(transformIndex);
799
+ this.pendingTemporalTransforms.delete(transformIndex);
800
+ this.pendingTemporalUploads.delete(transformIndex);
745
801
  }
746
802
  for (let ordinal = 0; ordinal < allocation.drawCount; ordinal += 1) {
747
803
  const drawIndex = allocation.drawStart + ordinal;