@forgeax/engine-render 0.1.31 → 0.1.33

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 (293) hide show
  1. package/README.md +447 -34
  2. package/dist/assembly/analytic-fog-registration.d.ts +3 -0
  3. package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
  4. package/dist/assembly/fxaa-registration.d.ts +4 -0
  5. package/dist/assembly/fxaa-registration.d.ts.map +1 -0
  6. package/dist/assembly/lod-observation.d.ts +4 -0
  7. package/dist/assembly/lod-observation.d.ts.map +1 -0
  8. package/dist/assembly/motion-blur-registration.d.ts +3 -0
  9. package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
  10. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  11. package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
  12. package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
  13. package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
  14. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
  15. package/dist/assembly/webgpu-ready.d.ts +3 -2
  16. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  17. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  18. package/dist/bloom-admission.d.ts +13 -0
  19. package/dist/bloom-admission.d.ts.map +1 -0
  20. package/dist/{chunk-FUUO4UND.mjs → chunk-3RTCJTH4.mjs} +12 -9
  21. package/dist/chunk-3RTCJTH4.mjs.map +1 -0
  22. package/dist/{chunk-NO4WZCAH.mjs → chunk-LEAXOMYJ.mjs} +3 -3
  23. package/dist/{chunk-NO4WZCAH.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
  24. package/dist/{chunk-JRBDO2KB.mjs → chunk-NB2S7ZRX.mjs} +166 -10
  25. package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
  26. package/dist/{chunk-7MQ3XBSP.mjs → chunk-NNQL7CB3.mjs} +4 -4
  27. package/dist/{chunk-7MQ3XBSP.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
  28. package/dist/{chunk-C3DDSXYK.mjs → chunk-OO3DRYK7.mjs} +171 -230
  29. package/dist/chunk-OO3DRYK7.mjs.map +1 -0
  30. package/dist/{chunk-S5UE2T5P.mjs → chunk-PLAXOOX5.mjs} +608 -7
  31. package/dist/chunk-PLAXOOX5.mjs.map +1 -0
  32. package/dist/{chunk-PSEQ6QPH.mjs → chunk-VOZWDM3L.mjs} +295 -198
  33. package/dist/chunk-VOZWDM3L.mjs.map +1 -0
  34. package/dist/{chunk-MDQJPY3T.mjs → chunk-ZDQWGQW7.mjs} +10 -4
  35. package/dist/chunk-ZDQWGQW7.mjs.map +1 -0
  36. package/dist/components/atmosphere.d.ts +2 -0
  37. package/dist/components/atmosphere.d.ts.map +1 -1
  38. package/dist/components/camera.d.ts +20 -4
  39. package/dist/components/camera.d.ts.map +1 -1
  40. package/dist/components/depth-of-field.d.ts +28 -0
  41. package/dist/components/depth-of-field.d.ts.map +1 -0
  42. package/dist/components/directional-light.d.ts +2 -1
  43. package/dist/components/directional-light.d.ts.map +1 -1
  44. package/dist/components/index.d.ts +1 -0
  45. package/dist/components/index.d.ts.map +1 -1
  46. package/dist/components/mesh-filter.d.ts +6 -5
  47. package/dist/components/mesh-filter.d.ts.map +1 -1
  48. package/dist/components/motion-blur.d.ts +1 -0
  49. package/dist/components/motion-blur.d.ts.map +1 -1
  50. package/dist/construct-renderer.mjs +3746 -837
  51. package/dist/construct-renderer.mjs.map +1 -1
  52. package/dist/device/gpu-residency.d.ts +6 -0
  53. package/dist/device/gpu-residency.d.ts.map +1 -1
  54. package/dist/environment/analytic-fog.d.ts +6 -0
  55. package/dist/environment/analytic-fog.d.ts.map +1 -0
  56. package/dist/errors/render.d.ts +6 -2
  57. package/dist/errors/render.d.ts.map +1 -1
  58. package/dist/extract/camera.d.ts.map +1 -1
  59. package/dist/extract/contracts.d.ts +7 -0
  60. package/dist/extract/contracts.d.ts.map +1 -1
  61. package/dist/extract/environment.d.ts +2 -0
  62. package/dist/extract/environment.d.ts.map +1 -1
  63. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts +19 -0
  64. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts.map +1 -0
  65. package/dist/features/depth-of-field/depth-of-field-feature.d.ts +43 -0
  66. package/dist/features/depth-of-field/depth-of-field-feature.d.ts.map +1 -0
  67. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts +34 -0
  68. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts.map +1 -0
  69. package/dist/features/depth-of-field/depth-of-field-params.d.ts +83 -0
  70. package/dist/features/depth-of-field/depth-of-field-params.d.ts.map +1 -0
  71. package/dist/features/error-reporting.d.ts +13 -0
  72. package/dist/features/error-reporting.d.ts.map +1 -0
  73. package/dist/features/host.d.ts +5 -0
  74. package/dist/features/host.d.ts.map +1 -1
  75. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  76. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  77. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  78. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  79. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  80. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  81. package/dist/features/plan.d.ts +7 -4
  82. package/dist/features/plan.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +9 -5
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/render-graph-compute.d.ts.map +1 -1
  86. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  87. package/dist/features/types.d.ts +19 -1
  88. package/dist/features/types.d.ts.map +1 -1
  89. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  90. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  91. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  92. package/dist/gpu-driven/production-raster.d.ts +3 -1
  93. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  94. package/dist/gpu-driven/view-gpu.d.ts +9 -0
  95. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +6 -1
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/index.d.ts +8 -3
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.mjs +6 -5
  101. package/dist/index.mjs.map +1 -1
  102. package/dist/inspection-types.d.ts +78 -2
  103. package/dist/inspection-types.d.ts.map +1 -1
  104. package/dist/instances.d.ts +15 -1
  105. package/dist/instances.d.ts.map +1 -1
  106. package/dist/internal.mjs +6 -6
  107. package/dist/mesh-material-bindings.d.ts +3 -0
  108. package/dist/mesh-material-bindings.d.ts.map +1 -1
  109. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  110. package/dist/pipeline/standard-output/types.d.ts +4 -2
  111. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  112. package/dist/pipeline/standard-post.d.ts +28 -19
  113. package/dist/pipeline/standard-post.d.ts.map +1 -1
  114. package/dist/pipeline-spec.d.ts +2 -2
  115. package/dist/plugin.d.ts.map +1 -1
  116. package/dist/record/frame-snapshot.d.ts +24 -1
  117. package/dist/record/frame-snapshot.d.ts.map +1 -1
  118. package/dist/record/frame-targets.d.ts.map +1 -1
  119. package/dist/record/frame.d.ts +5 -2
  120. package/dist/record/frame.d.ts.map +1 -1
  121. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +6 -5
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/render-context.d.ts +12 -28
  126. package/dist/record/render-context.d.ts.map +1 -1
  127. package/dist/record/shadow-pass.d.ts.map +1 -1
  128. package/dist/record/skybox-post-pass.d.ts +5 -3
  129. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  130. package/dist/record/typed-frame-graph.d.ts +8 -1
  131. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  133. package/dist/render-contract.d.ts +32 -5
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-graph-primitives.d.ts +2 -0
  136. package/dist/render-graph-primitives.d.ts.map +1 -1
  137. package/dist/render-pipeline.d.ts +15 -1
  138. package/dist/render-pipeline.d.ts.map +1 -1
  139. package/dist/render-system-extract-tail.d.ts.map +1 -1
  140. package/dist/render-system-extract.d.ts +26 -5
  141. package/dist/render-system-extract.d.ts.map +1 -1
  142. package/dist/render-system.d.ts +9 -2
  143. package/dist/render-system.d.ts.map +1 -1
  144. package/dist/scene/render-scene.d.ts +2 -0
  145. package/dist/scene/render-scene.d.ts.map +1 -1
  146. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  147. package/dist/temporal/frame.d.ts +7 -0
  148. package/dist/temporal/frame.d.ts.map +1 -1
  149. package/dist/temporal/index.mjs +5 -5
  150. package/dist/temporal/view.d.ts +6 -1
  151. package/dist/temporal/view.d.ts.map +1 -1
  152. package/dist/typed-render-graph-primitives.d.ts +10 -7
  153. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  154. package/dist/volume/component.d.ts +0 -1
  155. package/dist/volume/component.d.ts.map +1 -1
  156. package/dist/volume/passes.d.ts.map +1 -1
  157. package/dist/volume/temporal.d.ts +3 -1
  158. package/dist/volume/temporal.d.ts.map +1 -1
  159. package/package.json +21 -21
  160. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  161. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  162. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  163. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  164. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  165. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  166. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  167. package/src/__tests__/factory-contract.integration.test.ts +331 -68
  168. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  169. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  170. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +12 -0
  171. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  172. package/src/__tests__/gpu-driven-production.integration.test.ts +27 -1
  173. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
  174. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  175. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
  176. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  177. package/src/__tests__/gpu-scene.unit.test.ts +91 -0
  178. package/src/__tests__/instances-store.unit.test.ts +90 -0
  179. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  180. package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
  181. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +49 -0
  182. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  183. package/src/__tests__/public-surface.test-d.ts +8 -0
  184. package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
  185. package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
  186. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  187. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
  188. package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
  189. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  190. package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
  191. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  192. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  193. package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
  194. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  195. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  196. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  197. package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
  198. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
  199. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
  200. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  201. package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
  202. package/src/assembly/analytic-fog-registration.ts +14 -0
  203. package/src/assembly/fxaa-registration.ts +17 -0
  204. package/src/assembly/lod-observation.ts +12 -0
  205. package/src/assembly/motion-blur-registration.ts +17 -0
  206. package/src/assembly/renderer-facade.ts +2 -0
  207. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  208. package/src/assembly/webgpu-ready-contract.ts +8 -3
  209. package/src/assembly/webgpu-ready.ts +186 -250
  210. package/src/assembly/webgpu-renderer.ts +30 -45
  211. package/src/bloom-admission.ts +21 -0
  212. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  213. package/src/components/atmosphere.ts +4 -0
  214. package/src/components/camera.ts +66 -10
  215. package/src/components/depth-of-field.ts +45 -0
  216. package/src/components/directional-light.ts +2 -1
  217. package/src/components/index.ts +1 -0
  218. package/src/components/mesh-filter.ts +11 -9
  219. package/src/components/motion-blur.ts +1 -0
  220. package/src/device/gpu-residency.ts +56 -12
  221. package/src/environment/analytic-fog.ts +44 -0
  222. package/src/environment/background.ts +2 -2
  223. package/src/errors/render.ts +14 -1
  224. package/src/extract/camera.ts +42 -9
  225. package/src/extract/contracts.ts +7 -0
  226. package/src/extract/environment.ts +4 -0
  227. package/src/features/depth-of-field/__tests__/depth-of-field-params.unit.test.ts +943 -0
  228. package/src/features/depth-of-field/depth-of-field-assembly.ts +100 -0
  229. package/src/features/depth-of-field/depth-of-field-feature.ts +295 -0
  230. package/src/features/depth-of-field/depth-of-field-inspection.ts +237 -0
  231. package/src/features/depth-of-field/depth-of-field-params.ts +556 -0
  232. package/src/features/error-reporting.ts +78 -0
  233. package/src/features/host.ts +26 -13
  234. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  235. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  236. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  237. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  238. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  239. package/src/features/plan.ts +25 -8
  240. package/src/features/prepared-gpu-work.ts +86 -35
  241. package/src/features/render-graph-compute.ts +13 -5
  242. package/src/features/render-graph-contribution.ts +2 -0
  243. package/src/features/types.ts +18 -1
  244. package/src/gpu-driven/material-artifacts.ts +2 -7
  245. package/src/gpu-driven/prepared-draw.ts +15 -10
  246. package/src/gpu-driven/production-raster.ts +83 -7
  247. package/src/gpu-driven/view-gpu.ts +17 -0
  248. package/src/gpu-scene.ts +188 -21
  249. package/src/index.ts +64 -0
  250. package/src/inspection-types.ts +98 -2
  251. package/src/instances.ts +93 -0
  252. package/src/mesh-material-bindings.ts +4 -0
  253. package/src/pipeline/standard-output/graph.ts +3 -0
  254. package/src/pipeline/standard-output/types.ts +4 -1
  255. package/src/pipeline/standard-post.ts +324 -76
  256. package/src/pipeline-spec.ts +5 -5
  257. package/src/plugin.ts +2 -0
  258. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  259. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
  260. package/src/record/frame-snapshot.ts +31 -2
  261. package/src/record/frame-targets.ts +10 -6
  262. package/src/record/frame.ts +202 -28
  263. package/src/record/main-pass-geometry.ts +30 -1
  264. package/src/record/main-pass-sprite-draws.ts +6 -1
  265. package/src/record/main-pass.ts +1 -5
  266. package/src/record/mesh-ssbo.ts +43 -6
  267. package/src/record/render-context.ts +12 -28
  268. package/src/record/shadow-pass.ts +6 -1
  269. package/src/record/skybox-post-pass.ts +122 -196
  270. package/src/record/typed-frame-graph.ts +259 -15
  271. package/src/recovery/render-system-candidate.ts +28 -10
  272. package/src/render-contract.ts +34 -4
  273. package/src/render-graph-primitives.ts +8 -1
  274. package/src/render-pipeline.ts +22 -1
  275. package/src/render-system-extract-tail.ts +103 -13
  276. package/src/render-system-extract.ts +148 -38
  277. package/src/render-system.ts +332 -119
  278. package/src/scene/render-scene.ts +85 -4
  279. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  280. package/src/temporal/frame-coordinator.ts +8 -1
  281. package/src/temporal/frame.ts +20 -0
  282. package/src/temporal/view.ts +13 -2
  283. package/src/typed-render-graph-primitives.ts +124 -48
  284. package/src/volume/component.ts +0 -1
  285. package/src/volume/passes.ts +8 -4
  286. package/src/volume/resources.ts +1 -1
  287. package/src/volume/temporal.ts +11 -0
  288. package/dist/chunk-C3DDSXYK.mjs.map +0 -1
  289. package/dist/chunk-FUUO4UND.mjs.map +0 -1
  290. package/dist/chunk-JRBDO2KB.mjs.map +0 -1
  291. package/dist/chunk-MDQJPY3T.mjs.map +0 -1
  292. package/dist/chunk-PSEQ6QPH.mjs.map +0 -1
  293. package/dist/chunk-S5UE2T5P.mjs.map +0 -1
@@ -18,6 +18,12 @@ export function graphExecutionPhase(passName: string): RenderGraphExecutionPhase
18
18
  return 'record/graph-execute/ssr-reflection-mip';
19
19
  }
20
20
  if (passName === 'ssr-compose') return 'record/graph-execute/ssr-compose';
21
+ if (passName.startsWith('bloom-downsample-')) {
22
+ return 'record/graph-execute/bloom-downsample';
23
+ }
24
+ if (passName.startsWith('bloom-upsample-')) {
25
+ return 'record/graph-execute/bloom-upsample';
26
+ }
21
27
  switch (passName) {
22
28
  case 'cluster-binner-upload':
23
29
  case 'cluster-membership-producer':
@@ -47,12 +53,10 @@ export function graphExecutionPhase(passName: string): RenderGraphExecutionPhase
47
53
  return 'record/graph-execute/main';
48
54
  case 'fxaa':
49
55
  return 'record/graph-execute/fxaa';
50
- case 'bloom-bright':
51
- return 'record/graph-execute/bloom-bright';
52
- case 'bloom-blur-h':
53
- return 'record/graph-execute/bloom-blur-h';
54
- case 'bloom-blur-v':
55
- return 'record/graph-execute/bloom-blur-v';
56
+ case 'bloom-downsample':
57
+ return 'record/graph-execute/bloom-downsample';
58
+ case 'bloom-upsample':
59
+ return 'record/graph-execute/bloom-upsample';
56
60
  case 'bloom-composite':
57
61
  return 'record/graph-execute/bloom-composite';
58
62
  default:
@@ -1,3 +1,5 @@
1
+ import { encodeAnalyticFogParams } from '../environment/analytic-fog';
2
+ import type { FogFrame } from '../extract/environment';
1
3
  import { collectGpuDrivenMaterialArtifacts } from '../gpu-driven/material-artifacts';
2
4
  import { materialBindingKey, projectMaterialBindingClasses } from '../gpu-driven/material-bindings';
3
5
  import { uploadMaterialUniforms } from './material-uniforms';
@@ -5,7 +7,7 @@ import { uploadMaterialUniforms } from './material-uniforms';
5
7
  // Extracted from render-system-record.ts (feat-20260704 M3/w17, pure move).
6
8
 
7
9
  import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
8
- import type { World } from '@forgeax/engine-ecs';
10
+ import { Time, type World } from '@forgeax/engine-ecs';
9
11
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
10
12
  import { deriveVertexCount } from '@forgeax/engine-geometry';
11
13
  import { frustum, mat4, vec3 } from '@forgeax/engine-math';
@@ -34,8 +36,17 @@ import {
34
36
  RendererOperationError,
35
37
  } from '../errors/render';
36
38
  import { gpuDrivenDrawKey, gpuDrivenSourceDrawItemIndex } from '../extract/gpu-driven';
39
+ import {
40
+ DEPTH_OF_FIELD_MSAA_POST_PROCESS_IDS,
41
+ DEPTH_OF_FIELD_POST_PROCESS_IDS,
42
+ } from '../features/depth-of-field/depth-of-field-feature';
43
+ import {
44
+ packDepthOfFieldParams,
45
+ resolveDepthOfFieldFrameParams,
46
+ } from '../features/depth-of-field/depth-of-field-params';
37
47
  import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
38
48
  import type { ShadowViewIdentity } from '../gpu-driven/shadow-views';
49
+ import type { GpuDrivenLodSubmitIdentity } from '../gpu-driven/view-gpu';
39
50
  import { GpuBuffer } from '../gpu-resource';
40
51
  import { GPU_TEXTURE_USAGE_COPY_SRC } from '../gpu-texture-usage';
41
52
  import {
@@ -296,6 +307,14 @@ export async function observeCurrentFrame(
296
307
  }
297
308
 
298
309
  import { acknowledgeRenderFeatureSubmission } from '../features/host';
310
+ import {
311
+ effectiveMotionBlurSampleCount,
312
+ isMotionBlurIntervalValid,
313
+ MOTION_BLUR_PARAMS_BYTE_SIZE,
314
+ motionBlurExposureScale,
315
+ motionBlurSampleDelta,
316
+ motionBlurTemporalDemand,
317
+ } from '../features/motion-blur/motion-blur-params';
299
318
  import { createTemporalView, resolveTemporalReset, type TemporalView } from '../temporal/view';
300
319
  import { inspectVolumetricFog } from '../volume/inspection';
301
320
  import {
@@ -323,6 +342,7 @@ import {
323
342
  executeCompiledFrameGraph,
324
343
  getRenderFeatureGraphState,
325
344
  type RenderFeatureGraphCandidate,
345
+ settleDepthOfFieldGraphCandidate,
326
346
  settleVolumetricFogGraphCandidate,
327
347
  } from './typed-frame-graph';
328
348
  import {
@@ -895,6 +915,7 @@ function volumeTemporalSignature(
895
915
  densityGeneration: prepared.densityGeneration,
896
916
  width,
897
917
  height,
918
+ ...(fog.worldTimeSeconds === undefined ? {} : { worldTimeSeconds: fog.worldTimeSeconds }),
898
919
  ...(fog.lightKind === undefined ? {} : { lightKind: fog.lightKind }),
899
920
  ...(fog.lightEntity === undefined ? {} : { lightEntity: fog.lightEntity }),
900
921
  ...(fog.pointLightEntity === undefined ? {} : { pointLightEntity: fog.pointLightEntity }),
@@ -999,7 +1020,7 @@ function prepareVolumetricFogFrame(
999
1020
  }
1000
1021
  return undefined;
1001
1022
  }
1002
- const params = new Float32Array(32);
1023
+ const params = new Float32Array(36);
1003
1024
  params.set(fog.fog.bounds.min, 0);
1004
1025
  params.set(fog.fog.bounds.max, 4);
1005
1026
  params.set(fog.fog.extinction, 8);
@@ -1020,10 +1041,8 @@ function prepareVolumetricFogFrame(
1020
1041
  const lightColor = selectedSpot?.color ?? lights.directional?.color ?? vec3.create(0, 0, 0);
1021
1042
  if (lightDirection === undefined || lightColor === undefined) return undefined;
1022
1043
  params.set(lightDirection, 20);
1023
- // Reuse the vec4 padding lane as the renderer-owned frame phase for
1024
- // sub-froxel jitter. The temporal resolver consumes it without growing the
1025
- // public fog component or allocating another uniform buffer.
1026
- params[23] = frameState.frameNumber % 1024;
1044
+ // Reserved padding: render frame identity must not move volume samples.
1045
+ params[23] = 0;
1027
1046
  const selectedPointIndex =
1028
1047
  selectedPoint === undefined
1029
1048
  ? -1
@@ -1056,6 +1075,10 @@ function prepareVolumetricFogFrame(
1056
1075
  frameState.temporalFrame !== undefined && frameState.temporalFrame.resetReason === 'none'
1057
1076
  ? 1
1058
1077
  : 0;
1078
+ // World elapsed time is the sole authored-medium clock. The adjacent
1079
+ // frame phase above remains the deterministic sub-froxel jitter identity;
1080
+ // it never advances density motion.
1081
+ params[32] = fog.worldTimeSeconds ?? 0;
1059
1082
 
1060
1083
  const paramsBuffer = stageVolumetricFogParams(internals, frameState, params);
1061
1084
  if (paramsBuffer === undefined) return undefined;
@@ -1210,6 +1233,8 @@ function mapSsrHistoryResetReason(
1210
1233
  case 'environment-change':
1211
1234
  case 'fog-change':
1212
1235
  return 'reflection-generation';
1236
+ case 'time-discontinuity':
1237
+ return 'camera-cut';
1213
1238
  case 'device-recover':
1214
1239
  return 'device-recovery';
1215
1240
  case 'submit-failure':
@@ -1218,6 +1243,50 @@ function mapSsrHistoryResetReason(
1218
1243
  }
1219
1244
  }
1220
1245
 
1246
+ interface FrameClockSample {
1247
+ readonly deltaSeconds: number;
1248
+ readonly elapsedSeconds: number | undefined;
1249
+ /** Explicit host/replay clock was present but invalid. */
1250
+ readonly invalidSampleTime: boolean;
1251
+ }
1252
+
1253
+ /** Read the render sample clock from the camera's composition world once per frame. */
1254
+ function frameClockSample(
1255
+ world: World,
1256
+ worlds: readonly World[],
1257
+ camera: CameraSnapshot,
1258
+ sampleTimeSeconds?: number,
1259
+ ): FrameClockSample {
1260
+ const owner = worlds[camera.worldId ?? 0] ?? world;
1261
+ const invalidSampleTime = sampleTimeSeconds !== undefined && !Number.isFinite(sampleTimeSeconds);
1262
+ try {
1263
+ const clock = owner.getResource(Time);
1264
+ return {
1265
+ deltaSeconds: Number.isFinite(clock.delta) ? Math.max(0, clock.delta) : 0,
1266
+ // Keep the raw host sample even when ECS accepted only a clamped
1267
+ // simulation delta. Temporal admission compares this value with the
1268
+ // last successfully submitted sample and therefore sees real hitches.
1269
+ elapsedSeconds:
1270
+ sampleTimeSeconds === undefined
1271
+ ? Number.isFinite(clock.elapsed)
1272
+ ? clock.elapsed
1273
+ : undefined
1274
+ : Number.isFinite(sampleTimeSeconds)
1275
+ ? sampleTimeSeconds
1276
+ : undefined,
1277
+ invalidSampleTime,
1278
+ };
1279
+ } catch {
1280
+ // Test and recovery callers may provide a minimal World-shaped owner. An
1281
+ // absent clock is an invalid temporal pair and therefore safely resets.
1282
+ return {
1283
+ deltaSeconds: 0,
1284
+ elapsedSeconds: Number.isFinite(sampleTimeSeconds) ? sampleTimeSeconds : undefined,
1285
+ invalidSampleTime,
1286
+ };
1287
+ }
1288
+ }
1289
+
1221
1290
  function stageSsrHistoryCandidate(
1222
1291
  internals: RenderSystemInternals,
1223
1292
  frameState: RenderFrameState,
@@ -1602,6 +1671,7 @@ export function recordFrame(
1602
1671
  readonly activeEntityKeys?: ReadonlySet<number>;
1603
1672
  readonly activeEntityRevision?: number;
1604
1673
  readonly telemetryCandidateCount?: number;
1674
+ readonly telemetrySubmit?: GpuDrivenLodSubmitIdentity;
1605
1675
  readonly occlusion?: OcclusionFrameProjection;
1606
1676
  },
1607
1677
  renderReadLeases?: readonly RenderReadLease[],
@@ -1622,6 +1692,9 @@ export function recordFrame(
1622
1692
  preparedLighting?: ReturnType<typeof prepareFrameLighting>,
1623
1693
  ssrDependencies?: SsrDependenciesInspection,
1624
1694
  onRenderableDraw?: (entry: ValidatedRenderable) => void,
1695
+ analyticFog?: FogFrame,
1696
+ sampleTimeSeconds?: number,
1697
+ temporalReset = false,
1625
1698
  ): boolean {
1626
1699
  void shadowCasterEntityKeys;
1627
1700
  frameState.reflectionFallbackObservationSource = undefined;
@@ -1671,8 +1744,17 @@ export function recordFrame(
1671
1744
  }),
1672
1745
  );
1673
1746
  }
1674
- const camera = activeCameras[0];
1675
- if (!camera) return false;
1747
+ const requestedCamera = activeCameras[0];
1748
+ if (!requestedCamera) return false;
1749
+ const frameDepthOfField = resolveDepthOfFieldFrameParams(
1750
+ requestedCamera.depthOfField,
1751
+ requestedCamera.depthOfFieldError,
1752
+ frameState.depthOfFieldAccepted?.params,
1753
+ );
1754
+ const camera: CameraSnapshot =
1755
+ frameDepthOfField === undefined || frameDepthOfField === requestedCamera.depthOfField
1756
+ ? requestedCamera
1757
+ : { ...requestedCamera, depthOfField: frameDepthOfField };
1676
1758
  frameState.ssrRequested = camera.screenSpaceReflection !== undefined;
1677
1759
  const pipelineState = internals.getPipelineState();
1678
1760
  if (pipelineState === null) return false;
@@ -2028,6 +2110,9 @@ export function recordFrame(
2028
2110
  ...(frameGpuDriven.telemetryCandidateCount === undefined
2029
2111
  ? {}
2030
2112
  : { telemetryCandidateCount: frameGpuDriven.telemetryCandidateCount }),
2113
+ ...(frameGpuDriven.telemetrySubmit === undefined
2114
+ ? {}
2115
+ : { telemetrySubmit: frameGpuDriven.telemetrySubmit }),
2031
2116
  ...(frameGpuDriven.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
2032
2117
  ...(frameGpuDriven.scene?.structureMetrics === undefined
2033
2118
  ? {}
@@ -2172,6 +2257,7 @@ export function recordFrame(
2172
2257
  undefined,
2173
2258
  ssrAdmission,
2174
2259
  renderExtent,
2260
+ analyticFog !== undefined && analyticFog.density > 0 && analyticFog.maxOpacity > 0,
2175
2261
  ),
2176
2262
  );
2177
2263
  if (preflightGraph === null) {
@@ -2250,6 +2336,38 @@ export function recordFrame(
2250
2336
  const targetH = swapTarget.targetH;
2251
2337
 
2252
2338
  const previousTemporalView = frameState.lastSuccessfulTemporalView;
2339
+ const motionBlurDemand =
2340
+ camera.motionBlur === undefined
2341
+ ? false
2342
+ : motionBlurTemporalDemand({
2343
+ shutterAngle: camera.motionBlur.shutterAngle,
2344
+ maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
2345
+ sampleCount: camera.motionBlur.sampleCount,
2346
+ targetFps: camera.motionBlur.targetFps ?? 60,
2347
+ });
2348
+ const temporalDemand = camera.antialias === 'taa' || motionBlurDemand;
2349
+ const clock = frameClockSample(world, worlds, camera, sampleTimeSeconds);
2350
+ const deltaSeconds =
2351
+ temporalReset || clock.invalidSampleTime
2352
+ ? Number.NaN
2353
+ : motionBlurSampleDelta(
2354
+ clock.elapsedSeconds,
2355
+ previousTemporalView?.sampleTimeSeconds,
2356
+ clock.deltaSeconds,
2357
+ );
2358
+ const intervalValid = isMotionBlurIntervalValid(deltaSeconds);
2359
+ // An off MB-only frame still carries an unjittered temporal view so the
2360
+ // accepted sample clock remains observable. Its history flag is false,
2361
+ // however, and must form a new baseline when the effect is enabled again.
2362
+ // A reset frame already has an explicit reason and is the valid baseline
2363
+ // for the following pair, so it is deliberately excluded here.
2364
+ const motionBlurFirstFrame =
2365
+ motionBlurDemand &&
2366
+ camera.antialias !== 'taa' &&
2367
+ (previousTemporalView === undefined ||
2368
+ (previousTemporalView.mode === 'off' &&
2369
+ !previousTemporalView.historyValid &&
2370
+ previousTemporalView.resetReason === undefined));
2253
2371
  const currentUnjitteredViewProjection = mat4.multiply(
2254
2372
  mat4.create(),
2255
2373
  computeProjectionMatrix(camera),
@@ -2276,19 +2394,32 @@ export function recordFrame(
2276
2394
  internalHeight: renderExtent.internalHeight,
2277
2395
  }),
2278
2396
  ...(camera.historyVersion === undefined ? {} : { historyVersion: camera.historyVersion }),
2397
+ ...(clock.elapsedSeconds === undefined ? {} : { sampleTimeSeconds: clock.elapsedSeconds }),
2279
2398
  historyValid: false,
2280
2399
  currentUnjitteredViewProjection,
2281
2400
  currentCameraPosition,
2282
2401
  });
2283
2402
  const resetReason =
2284
- camera.antialias === 'taa' && environmentReady
2285
- ? resolveTemporalReset(previousTemporalView, temporalCandidate)
2403
+ temporalDemand && environmentReady
2404
+ ? (resolveTemporalReset(previousTemporalView, temporalCandidate) ??
2405
+ (temporalReset || (motionBlurDemand && !intervalValid)
2406
+ ? 'time-discontinuity'
2407
+ : motionBlurFirstFrame
2408
+ ? 'first-frame'
2409
+ : undefined))
2286
2410
  : undefined;
2287
2411
  const canReusePrevious = previousTemporalView !== undefined && resetReason === undefined;
2288
2412
  const temporalView = createTemporalView({
2289
2413
  ...temporalCandidate.input,
2290
2414
  frameIndex: resetReason === undefined ? temporalCandidate.input.frameIndex : 0,
2291
- historyValid: camera.antialias === 'taa' && environmentReady && canReusePrevious,
2415
+ // Motion Blur needs a finite positive pair before it can read the
2416
+ // baseline. TAA-only frames retain their existing temporal contract and
2417
+ // do not depend on the simulation clock being present in a test host.
2418
+ historyValid:
2419
+ temporalDemand &&
2420
+ environmentReady &&
2421
+ canReusePrevious &&
2422
+ (!motionBlurDemand || intervalValid),
2292
2423
  resetReason,
2293
2424
  ...(canReusePrevious && previousTemporalView.currentUnjitteredViewProjection !== undefined
2294
2425
  ? {
@@ -2339,17 +2470,26 @@ export function recordFrame(
2339
2470
  );
2340
2471
  activeVolumetricFogContext = resolvedHistory.context;
2341
2472
  activeVolumetricFogSignature = resolvedHistory.signature;
2342
- const historyFlag = new Float32Array([activeVolumetricFogContext.historyValid ? 1 : 0]);
2473
+ const previousTime = frameState.volumetricFogHistorySignature?.worldTimeSeconds;
2474
+ const currentTime = resolvedHistory.signature.worldTimeSeconds;
2475
+ // Adjacent UBO lanes: validity, World time, elapsed time since accepted history.
2476
+ const historyParams = new Float32Array([
2477
+ activeVolumetricFogContext.historyValid ? 1 : 0,
2478
+ currentTime ?? 0,
2479
+ previousTime === undefined || currentTime === undefined
2480
+ ? 0
2481
+ : Math.max(0, currentTime - previousTime),
2482
+ ]);
2343
2483
  if (frameState.volumetricFogPendingParams !== undefined) {
2344
- frameState.volumetricFogPendingParams[31] = historyFlag[0] ?? 0;
2484
+ frameState.volumetricFogPendingParams.set(historyParams, 31);
2345
2485
  }
2346
- const historyFlagWritten = internals.device.queue.writeBuffer(
2486
+ const historyParamsWritten = internals.device.queue.writeBuffer(
2347
2487
  activeVolumetricFogContext.paramsBuffer,
2348
2488
  31 * Float32Array.BYTES_PER_ELEMENT,
2349
- historyFlag,
2489
+ historyParams,
2350
2490
  );
2351
- if (!historyFlagWritten.ok) {
2352
- internals.errorRegistry.fire(historyFlagWritten.error);
2491
+ if (!historyParamsWritten.ok) {
2492
+ internals.errorRegistry.fire(historyParamsWritten.error);
2353
2493
  abortPreparedGpuDriven();
2354
2494
  return false;
2355
2495
  }
@@ -2634,14 +2774,47 @@ export function recordFrame(
2634
2774
  // builtin motion-blur pass receives its all-zero registration default and
2635
2775
  // becomes a no-op even though the temporal scene target is populated.
2636
2776
  const framePostProcessParams = new Map(postProcessParams);
2777
+ if (analyticFog !== undefined)
2778
+ framePostProcessParams.set(
2779
+ 'forgeax.analytic-fog',
2780
+ encodeAnalyticFogParams(analyticFog, recordCamera),
2781
+ );
2637
2782
  if (camera.motionBlur !== undefined) {
2638
- const motionBlurParams = new Uint8Array(16);
2639
- const motionBlurView = new DataView(motionBlurParams.buffer);
2640
- motionBlurView.setFloat32(0, camera.motionBlur.shutterAngle, true);
2641
- motionBlurView.setFloat32(4, camera.motionBlur.maxRadiusPixels, true);
2642
- motionBlurView.setUint32(8, camera.motionBlur.sampleCount, true);
2643
- motionBlurView.setUint32(12, temporalView?.historyValid === true ? 0 : 1, true);
2644
- framePostProcessParams.set('forgeax.motion-blur', motionBlurParams);
2783
+ if (motionBlurDemand) {
2784
+ const motionBlurParams = new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE);
2785
+ const motionBlurView = new DataView(motionBlurParams.buffer);
2786
+ const uniformDeltaSeconds = Number.isFinite(deltaSeconds) ? deltaSeconds : 0;
2787
+ motionBlurView.setFloat32(0, camera.motionBlur.shutterAngle, true);
2788
+ motionBlurView.setFloat32(4, camera.motionBlur.maxRadiusPixels, true);
2789
+ motionBlurView.setUint32(
2790
+ 8,
2791
+ effectiveMotionBlurSampleCount(camera.motionBlur.sampleCount),
2792
+ true,
2793
+ );
2794
+ motionBlurView.setUint32(12, temporalView?.historyValid === true ? 0 : 1, true);
2795
+ motionBlurView.setFloat32(16, camera.motionBlur.targetFps ?? 60, true);
2796
+ motionBlurView.setFloat32(
2797
+ 20,
2798
+ motionBlurExposureScale(uniformDeltaSeconds, camera.motionBlur.targetFps ?? 60),
2799
+ true,
2800
+ );
2801
+ motionBlurView.setFloat32(24, uniformDeltaSeconds, true);
2802
+ framePostProcessParams.set('forgeax.motion-blur', motionBlurParams);
2803
+ }
2804
+ }
2805
+ if (camera.depthOfField !== undefined) {
2806
+ const dofParams = packDepthOfFieldParams(camera.depthOfField, {
2807
+ outputHeight: targetH,
2808
+ near: camera.near,
2809
+ far: camera.far,
2810
+ useTemporalDepth: camera.antialias === 'taa' && temporalView !== undefined,
2811
+ });
2812
+ for (const id of [
2813
+ ...DEPTH_OF_FIELD_POST_PROCESS_IDS,
2814
+ ...DEPTH_OF_FIELD_MSAA_POST_PROCESS_IDS,
2815
+ ]) {
2816
+ framePostProcessParams.set(id, dofParams);
2817
+ }
2645
2818
  }
2646
2819
 
2647
2820
  const passCtx: _InternalRenderPipelineContext = {
@@ -2925,11 +3098,12 @@ export function recordFrame(
2925
3098
  frameState.ssrHistoryOwner?.abortFrame(frameState.ssrHistoryCandidate, 'submit');
2926
3099
  frameState.ssrHistoryCandidate = undefined;
2927
3100
  }
2928
- // Volume topology replacements are provisional until the queue submission
2929
- // succeeds. This runs for every early-return path as well, so a swapchain,
2930
- // encoder, or graph failure cannot leave a non-submitted candidate as the
2931
- // next frame's accepted graph.
3101
+ // DoF and volume topology replacements are provisional until the queue
3102
+ // submission succeeds. This runs for every early-return path as well, so a
3103
+ // swapchain, encoder, or graph failure cannot leave a non-submitted
3104
+ // candidate as the next frame's accepted graph.
2932
3105
  settleVolumetricFogGraphCandidate(frameState, submitted);
3106
+ settleDepthOfFieldGraphCandidate(frameState, submitted);
2933
3107
  if (volumeInspectionReady && (submitted || volumeSubmissionAttempted)) {
2934
3108
  updateVolumetricFogInspection(
2935
3109
  frameState,
@@ -1481,6 +1481,7 @@ function uploadInstanceRanges(
1481
1481
  ranges: readonly { readonly start: number; readonly end: number }[],
1482
1482
  storageBacked: boolean,
1483
1483
  sourceOffsetInstances = 0,
1484
+ previousInst: NonNullable<RenderableSnapshot['temporal']>['previousInstances'] = undefined,
1484
1485
  ): InstanceUploadResult {
1485
1486
  const strideBytes =
1486
1487
  (storageBacked ? INSTANCE_STORAGE_STRIDE_FLOATS : 16) * Float32Array.BYTES_PER_ELEMENT;
@@ -1494,7 +1495,31 @@ function uploadInstanceRanges(
1494
1495
  (sourceOffsetInstances + start) * 16,
1495
1496
  (sourceOffsetInstances + end) * 16,
1496
1497
  );
1497
- const payload = storageBacked ? packInstanceStorageBuffer(source) : source;
1498
+ // A generation-bearing previous collection is kept whole so a compacted
1499
+ // or reordered current range can resolve its real prior ordinal. Without
1500
+ // identity proof, keep the old range-local shape and let the packer seed
1501
+ // on count mismatch rather than pairing an unrelated slot.
1502
+ const hasIdentityProof =
1503
+ inst.generations !== undefined && previousInst?.generations !== undefined;
1504
+ const previousSource = hasIdentityProof
1505
+ ? previousInst?.transforms
1506
+ : previousInst?.transforms.subarray(
1507
+ (sourceOffsetInstances + start) * 16,
1508
+ (sourceOffsetInstances + end) * 16,
1509
+ );
1510
+ const currentGenerations = inst.generations?.subarray(
1511
+ sourceOffsetInstances + start,
1512
+ sourceOffsetInstances + end,
1513
+ );
1514
+ const previousGenerations = hasIdentityProof
1515
+ ? previousInst?.generations
1516
+ : previousInst?.generations?.subarray(
1517
+ sourceOffsetInstances + start,
1518
+ sourceOffsetInstances + end,
1519
+ );
1520
+ const payload = storageBacked
1521
+ ? packInstanceStorageBuffer(source, previousSource, currentGenerations, previousGenerations)
1522
+ : source;
1498
1523
  const written = c.runtime.device.queue.writeBuffer(buffer, start * strideBytes, payload);
1499
1524
  if (!written.ok) {
1500
1525
  c.runtime.errorRegistry.fire(written.error);
@@ -1673,6 +1698,7 @@ export function resolveStorageInstanceChunks(
1673
1698
  rangesToUpload,
1674
1699
  true,
1675
1700
  start,
1701
+ entry.source.temporal?.previousInstances,
1676
1702
  );
1677
1703
  for (const range of upload.ranges) {
1678
1704
  uploadedRanges.push({ start: range.start + start, end: range.end + start });
@@ -1858,6 +1884,7 @@ function resolveGeometryInstanceBuffer(
1858
1884
  rangesToUpload,
1859
1885
  false,
1860
1886
  start,
1887
+ entry.source.temporal?.previousInstances,
1861
1888
  );
1862
1889
  for (const range of upload.ranges) {
1863
1890
  uploadedRanges.push({ start: range.start + start, end: range.end + start });
@@ -1997,6 +2024,8 @@ function resolveGeometryInstanceBuffer(
1997
2024
  active.buffer.handle,
1998
2025
  ranges,
1999
2026
  !uniformFallback,
2027
+ 0,
2028
+ entry.source.temporal?.previousInstances,
2000
2029
  );
2001
2030
  uploadRanges = upload.ranges;
2002
2031
  uploadedBytes = upload.bytes;
@@ -690,7 +690,12 @@ function recordSpriteEntityDraws(
690
690
  {
691
691
  const instancePayload = uniformFallback
692
692
  ? spriteInst.transforms
693
- : packInstanceStorageBuffer(spriteInst.transforms);
693
+ : packInstanceStorageBuffer(
694
+ spriteInst.transforms,
695
+ spriteEntry.source.temporal?.previousInstances?.transforms,
696
+ spriteInst.generations,
697
+ spriteEntry.source.temporal?.previousInstances?.generations,
698
+ );
694
699
  const requestedBytes = instancePayload.byteLength;
695
700
  const cap = runtime.device.limits.maxStorageBufferBindingSize;
696
701
  if (typeof cap === 'number' && requestedBytes > cap) {
@@ -676,11 +676,7 @@ export function recordMainPass(
676
676
  hint: 'publish the current frame view/light/shadow bind group before GPU raster',
677
677
  });
678
678
  }
679
- if (
680
- gpuDrivenStandardPbrFrameResources === undefined ||
681
- gpuDrivenStandardPbrFrameResources.materialBindGroups.length === 0 ||
682
- gpuDrivenStandardPbrFrameResources.materialBindGroups.some((group) => group === undefined)
683
- ) {
679
+ if (gpuDrivenStandardPbrFrameResources === undefined) {
684
680
  throw new RhiError({
685
681
  code: 'rhi-not-available',
686
682
  expected: 'complete main-pass Standard PBR material bind-group receipt',
@@ -43,23 +43,58 @@ export const MAX_UNIFORM_INSTANCES = 128;
43
43
  /** The storage-backed InstanceData layout is current + previous mat4. */
44
44
  export const INSTANCE_STORAGE_STRIDE_FLOATS = 32;
45
45
 
46
+ function hasStableGenerationColumn(generations: Uint32Array | undefined, count: number): boolean {
47
+ if (generations === undefined || generations.length !== count) return false;
48
+ const seen = new Set<number>();
49
+ for (const generation of generations) {
50
+ if (generation === 0 || seen.has(generation)) return false;
51
+ seen.add(generation);
52
+ }
53
+ return true;
54
+ }
55
+
46
56
  /**
47
57
  * Project the ECS transform snapshot into the storage-backed InstanceData
48
- * layout. The extract contract carries one current mat4 per instance; until
49
- * the ECS snapshot owns a previous-instance column, current is the honest
50
- * previous value as well. Uniform-backed variants keep the compact 16-float
51
- * payload and do not use this projection.
58
+ * layout. Unique identity generations select the previous matrix only when
59
+ * the producer proves that the instance survived. Replaced slots seed
60
+ * previous=current; reordered slots map back to the real prior ordinal so a
61
+ * compaction cannot exchange velocities. Uniform-backed variants keep the
62
+ * compact 16-float payload and do not use this projection.
52
63
  */
53
- export function packInstanceStorageBuffer(transforms: Float32Array): Float32Array {
64
+ export function packInstanceStorageBuffer(
65
+ transforms: Float32Array,
66
+ previousTransforms: Float32Array = transforms,
67
+ generations?: Uint32Array,
68
+ previousGenerations?: Uint32Array,
69
+ ): Float32Array {
54
70
  const count = Math.floor(transforms.length / 16);
55
71
  const out = new Float32Array(count * INSTANCE_STORAGE_STRIDE_FLOATS);
72
+ const previousCount = Math.floor(previousTransforms.length / 16);
73
+ const generationInput = generations !== undefined || previousGenerations !== undefined;
74
+ const identityProof =
75
+ hasStableGenerationColumn(generations, count) &&
76
+ hasStableGenerationColumn(previousGenerations, previousCount);
77
+ const previousIndexByGeneration = identityProof
78
+ ? new Map(
79
+ Array.from(previousGenerations ?? [], (generation, index) => [generation, index] as const),
80
+ )
81
+ : undefined;
56
82
  for (let i = 0; i < count; i++) {
57
83
  const sourceBase = i * 16;
58
84
  const targetBase = i * INSTANCE_STORAGE_STRIDE_FLOATS;
85
+ const previousIndex = identityProof
86
+ ? previousIndexByGeneration?.get(generations?.[i] ?? 0)
87
+ : !generationInput && previousCount === count
88
+ ? i
89
+ : undefined;
90
+ const sameIdentity = previousIndex !== undefined && previousIndex < previousCount;
91
+ const previousBase = (previousIndex ?? i) * 16;
59
92
  for (let k = 0; k < 16; k++) {
60
93
  const value = transforms[sourceBase + k] ?? 0;
61
94
  out[targetBase + k] = value;
62
- out[targetBase + 16 + k] = value;
95
+ out[targetBase + 16 + k] = sameIdentity
96
+ ? (previousTransforms[previousBase + k] ?? value)
97
+ : value;
63
98
  }
64
99
  }
65
100
  return out;
@@ -451,6 +486,7 @@ export function uploadMeshSsboBatch(
451
486
  _meshSsboScratchFloats[slot + 38] = 1;
452
487
  _meshSsboScratchFloats[slot + 43] = 1;
453
488
  _meshSsboScratchFloats[slot + 44] = 1;
489
+ _meshSsboScratchFloats[slot + 45] = 1;
454
490
  } else {
455
491
  const worldFromLocal = entry.source.transform.world;
456
492
  for (let k = 0; k < 16; k++) _meshSsboScratchFloats[slot + k] = worldFromLocal[k] ?? 0;
@@ -483,6 +519,7 @@ export function uploadMeshSsboBatch(
483
519
  }
484
520
  _meshSsboScratchFloats[slot + 44] =
485
521
  entry.source.temporal?.reactive === true || reactiveMaterial ? 1 : 0;
522
+ _meshSsboScratchFloats[slot + 45] = entry.source.temporal?.motionValid === false ? 0 : 1;
486
523
  }
487
524
  }
488
525
  if (neededBytes > 0) {
@@ -337,17 +337,15 @@ export interface PerPassResources {
337
337
  readonly skyboxBindGroupLayout: BindGroupLayout | null;
338
338
  readonly skyboxSampler: Sampler | null;
339
339
  readonly skyboxRotationBuffer: Buffer | null;
340
- bloomBrightPipeline: RhiRenderPipeline | null;
341
- bloomBlurHPipeline: RhiRenderPipeline | null;
342
- bloomBlurVPipeline: RhiRenderPipeline | null;
340
+ bloomDownsamplePipeline: RhiRenderPipeline | null;
341
+ bloomUpsamplePipeline: RhiRenderPipeline | null;
343
342
  bloomCompositePipeline: RhiRenderPipeline | null;
344
- bloomBrightBindGroupLayout: BindGroupLayout | null;
345
- bloomBlurBindGroupLayout: BindGroupLayout | null;
343
+ bloomDownsampleBindGroupLayout: BindGroupLayout | null;
344
+ bloomUpsampleBindGroupLayout: BindGroupLayout | null;
346
345
  bloomCompositeBindGroupLayout: BindGroupLayout | null;
347
346
  bloomSampler: Sampler | null;
348
- bloomBrightParamsBuffer: Buffer | null;
349
- bloomBlurHParamsBuffer: Buffer | null;
350
- bloomBlurVParamsBuffer: Buffer | null;
347
+ bloomDownsampleParamsBuffer: Buffer | null;
348
+ bloomUpsampleParamsBuffer: Buffer | null;
351
349
  bloomCompositeParamsBuffer: Buffer | null;
352
350
  ensureBloomResources?: () => void;
353
351
  getBloomResources?: () => BloomPersistentBundle | null;
@@ -358,18 +356,6 @@ export interface PerPassResources {
358
356
  /** Drain candidate, active, and fence-retiring Bloom generations. */
359
357
  drainBloomResources?: () => void;
360
358
  inspectBloomResources?: (graph?: CompiledRenderGraphInfo) => BloomInspection;
361
- bloomBrightTexture: Texture | null;
362
- bloomBrightView: TextureView | null;
363
- bloomBrightWidth: number;
364
- bloomBrightHeight: number;
365
- bloomBlurHTexture: Texture | null;
366
- bloomBlurHView: TextureView | null;
367
- bloomBlurHWidth: number;
368
- bloomBlurHHeight: number;
369
- bloomBlurVTexture: Texture | null;
370
- bloomBlurVView: TextureView | null;
371
- bloomBlurVWidth: number;
372
- bloomBlurVHeight: number;
373
359
  ssaoCalcPipeline: RhiRenderPipeline | null;
374
360
  ssaoBlurPipeline: RhiRenderPipeline | null;
375
361
  ssaoBgl: BindGroupLayout | null;
@@ -392,17 +378,15 @@ export interface PerPassResources {
392
378
  export interface BloomPersistentBundle {
393
379
  readonly scope: DeviceScope;
394
380
  readonly generation: number;
395
- readonly bloomBrightPipeline: RhiRenderPipeline | null;
396
- readonly bloomBlurHPipeline: RhiRenderPipeline | null;
397
- readonly bloomBlurVPipeline: RhiRenderPipeline | null;
381
+ readonly bloomDownsamplePipeline: RhiRenderPipeline | null;
382
+ readonly bloomUpsamplePipeline: RhiRenderPipeline | null;
398
383
  readonly bloomCompositePipeline: RhiRenderPipeline | null;
399
- readonly bloomBrightBindGroupLayout: BindGroupLayout | null;
400
- readonly bloomBlurBindGroupLayout: BindGroupLayout | null;
384
+ readonly bloomDownsampleBindGroupLayout: BindGroupLayout | null;
385
+ readonly bloomUpsampleBindGroupLayout: BindGroupLayout | null;
401
386
  readonly bloomCompositeBindGroupLayout: BindGroupLayout | null;
402
387
  readonly bloomSampler: Sampler | null;
403
- readonly bloomBrightParamsBuffer: Buffer | null;
404
- readonly bloomBlurHParamsBuffer: Buffer | null;
405
- readonly bloomBlurVParamsBuffer: Buffer | null;
388
+ readonly bloomDownsampleParamsBuffer: Buffer | null;
389
+ readonly bloomUpsampleParamsBuffer: Buffer | null;
406
390
  readonly bloomCompositeParamsBuffer: Buffer | null;
407
391
  }
408
392