@forgeax/engine-render 0.1.32 → 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 (250) hide show
  1. package/README.md +379 -33
  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 +1 -1
  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-DRR7D3KX.mjs → chunk-3RTCJTH4.mjs} +12 -9
  21. package/dist/chunk-3RTCJTH4.mjs.map +1 -0
  22. package/dist/{chunk-S665NJNF.mjs → chunk-LEAXOMYJ.mjs} +3 -3
  23. package/dist/{chunk-S665NJNF.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
  24. package/dist/{chunk-6TFBXLRJ.mjs → chunk-NB2S7ZRX.mjs} +155 -11
  25. package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
  26. package/dist/{chunk-LULCA3OF.mjs → chunk-NNQL7CB3.mjs} +4 -4
  27. package/dist/{chunk-LULCA3OF.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
  28. package/dist/{chunk-4NNZQCL3.mjs → chunk-OO3DRYK7.mjs} +66 -200
  29. package/dist/chunk-OO3DRYK7.mjs.map +1 -0
  30. package/dist/{chunk-GWCEMDVR.mjs → chunk-PLAXOOX5.mjs} +232 -7
  31. package/dist/chunk-PLAXOOX5.mjs.map +1 -0
  32. package/dist/{chunk-JRIPCMYC.mjs → chunk-VOZWDM3L.mjs} +283 -193
  33. package/dist/chunk-VOZWDM3L.mjs.map +1 -0
  34. package/dist/{chunk-JCIRYQ4V.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/mesh-filter.d.ts +6 -5
  41. package/dist/components/mesh-filter.d.ts.map +1 -1
  42. package/dist/components/motion-blur.d.ts +1 -0
  43. package/dist/components/motion-blur.d.ts.map +1 -1
  44. package/dist/construct-renderer.mjs +2865 -526
  45. package/dist/construct-renderer.mjs.map +1 -1
  46. package/dist/device/gpu-residency.d.ts +6 -0
  47. package/dist/device/gpu-residency.d.ts.map +1 -1
  48. package/dist/environment/analytic-fog.d.ts +6 -0
  49. package/dist/environment/analytic-fog.d.ts.map +1 -0
  50. package/dist/errors/render.d.ts +6 -2
  51. package/dist/errors/render.d.ts.map +1 -1
  52. package/dist/extract/camera.d.ts.map +1 -1
  53. package/dist/extract/contracts.d.ts +7 -0
  54. package/dist/extract/contracts.d.ts.map +1 -1
  55. package/dist/extract/environment.d.ts +2 -0
  56. package/dist/extract/environment.d.ts.map +1 -1
  57. package/dist/features/error-reporting.d.ts +13 -0
  58. package/dist/features/error-reporting.d.ts.map +1 -0
  59. package/dist/features/host.d.ts +5 -0
  60. package/dist/features/host.d.ts.map +1 -1
  61. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  62. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  63. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  64. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  65. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  66. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  67. package/dist/features/plan.d.ts +7 -4
  68. package/dist/features/plan.d.ts.map +1 -1
  69. package/dist/features/prepared-gpu-work.d.ts +9 -5
  70. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  71. package/dist/features/render-graph-compute.d.ts.map +1 -1
  72. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  73. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  74. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  75. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  76. package/dist/gpu-driven/production-raster.d.ts +3 -1
  77. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  78. package/dist/gpu-driven/view-gpu.d.ts +9 -0
  79. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  80. package/dist/gpu-scene.d.ts +6 -1
  81. package/dist/gpu-scene.d.ts.map +1 -1
  82. package/dist/index.d.ts +5 -2
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.mjs +5 -5
  85. package/dist/inspection-types.d.ts +43 -2
  86. package/dist/inspection-types.d.ts.map +1 -1
  87. package/dist/instances.d.ts +8 -1
  88. package/dist/instances.d.ts.map +1 -1
  89. package/dist/internal.mjs +6 -6
  90. package/dist/pipeline/standard-post.d.ts +28 -19
  91. package/dist/pipeline/standard-post.d.ts.map +1 -1
  92. package/dist/pipeline-spec.d.ts +2 -2
  93. package/dist/record/frame-snapshot.d.ts +3 -0
  94. package/dist/record/frame-snapshot.d.ts.map +1 -1
  95. package/dist/record/frame-targets.d.ts.map +1 -1
  96. package/dist/record/frame.d.ts +5 -2
  97. package/dist/record/frame.d.ts.map +1 -1
  98. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  99. package/dist/record/mesh-ssbo.d.ts +6 -5
  100. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  101. package/dist/record/render-context.d.ts +12 -28
  102. package/dist/record/render-context.d.ts.map +1 -1
  103. package/dist/record/shadow-pass.d.ts.map +1 -1
  104. package/dist/record/skybox-post-pass.d.ts +5 -3
  105. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  106. package/dist/record/typed-frame-graph.d.ts +1 -1
  107. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  108. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  109. package/dist/render-contract.d.ts +24 -4
  110. package/dist/render-contract.d.ts.map +1 -1
  111. package/dist/render-pipeline.d.ts +3 -1
  112. package/dist/render-pipeline.d.ts.map +1 -1
  113. package/dist/render-system-extract-tail.d.ts.map +1 -1
  114. package/dist/render-system-extract.d.ts +8 -1
  115. package/dist/render-system-extract.d.ts.map +1 -1
  116. package/dist/render-system.d.ts +7 -1
  117. package/dist/render-system.d.ts.map +1 -1
  118. package/dist/scene/render-scene.d.ts +2 -0
  119. package/dist/scene/render-scene.d.ts.map +1 -1
  120. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  121. package/dist/temporal/frame.d.ts +7 -0
  122. package/dist/temporal/frame.d.ts.map +1 -1
  123. package/dist/temporal/index.mjs +5 -5
  124. package/dist/temporal/view.d.ts +6 -1
  125. package/dist/temporal/view.d.ts.map +1 -1
  126. package/dist/typed-render-graph-primitives.d.ts +10 -7
  127. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  128. package/dist/volume/component.d.ts +0 -1
  129. package/dist/volume/component.d.ts.map +1 -1
  130. package/dist/volume/passes.d.ts.map +1 -1
  131. package/dist/volume/temporal.d.ts +3 -1
  132. package/dist/volume/temporal.d.ts.map +1 -1
  133. package/package.json +21 -21
  134. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  135. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  136. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  137. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  138. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  139. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  140. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  141. package/src/__tests__/factory-contract.integration.test.ts +331 -68
  142. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  143. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  144. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  145. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
  146. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  147. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
  148. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  149. package/src/__tests__/gpu-scene.unit.test.ts +91 -0
  150. package/src/__tests__/instances-store.unit.test.ts +75 -0
  151. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  152. package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
  153. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
  154. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  155. package/src/__tests__/public-surface.test-d.ts +8 -0
  156. package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
  157. package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
  158. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  159. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
  160. package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
  161. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  162. package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
  163. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  164. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  165. package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
  166. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  167. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  168. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  169. package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
  170. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
  171. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
  172. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  173. package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
  174. package/src/assembly/analytic-fog-registration.ts +14 -0
  175. package/src/assembly/fxaa-registration.ts +17 -0
  176. package/src/assembly/lod-observation.ts +12 -0
  177. package/src/assembly/motion-blur-registration.ts +17 -0
  178. package/src/assembly/renderer-facade.ts +2 -0
  179. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  180. package/src/assembly/webgpu-ready-contract.ts +8 -3
  181. package/src/assembly/webgpu-ready.ts +181 -245
  182. package/src/assembly/webgpu-renderer.ts +16 -31
  183. package/src/bloom-admission.ts +21 -0
  184. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  185. package/src/components/atmosphere.ts +4 -0
  186. package/src/components/camera.ts +66 -10
  187. package/src/components/mesh-filter.ts +11 -9
  188. package/src/components/motion-blur.ts +1 -0
  189. package/src/device/gpu-residency.ts +56 -12
  190. package/src/environment/analytic-fog.ts +44 -0
  191. package/src/environment/background.ts +2 -2
  192. package/src/errors/render.ts +14 -1
  193. package/src/extract/camera.ts +20 -7
  194. package/src/extract/contracts.ts +7 -0
  195. package/src/extract/environment.ts +4 -0
  196. package/src/features/error-reporting.ts +78 -0
  197. package/src/features/host.ts +25 -12
  198. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  199. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  200. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  201. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  202. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  203. package/src/features/plan.ts +25 -8
  204. package/src/features/prepared-gpu-work.ts +86 -35
  205. package/src/features/render-graph-compute.ts +13 -5
  206. package/src/features/render-graph-contribution.ts +2 -0
  207. package/src/gpu-driven/material-artifacts.ts +2 -7
  208. package/src/gpu-driven/prepared-draw.ts +15 -10
  209. package/src/gpu-driven/production-raster.ts +83 -7
  210. package/src/gpu-driven/view-gpu.ts +17 -0
  211. package/src/gpu-scene.ts +188 -21
  212. package/src/index.ts +32 -0
  213. package/src/inspection-types.ts +60 -2
  214. package/src/instances.ts +80 -1
  215. package/src/pipeline/standard-post.ts +307 -78
  216. package/src/pipeline-spec.ts +5 -5
  217. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  218. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
  219. package/src/record/frame-snapshot.ts +5 -1
  220. package/src/record/frame-targets.ts +10 -6
  221. package/src/record/frame.ts +163 -22
  222. package/src/record/main-pass-geometry.ts +30 -1
  223. package/src/record/main-pass-sprite-draws.ts +6 -1
  224. package/src/record/mesh-ssbo.ts +43 -6
  225. package/src/record/render-context.ts +12 -28
  226. package/src/record/shadow-pass.ts +6 -1
  227. package/src/record/skybox-post-pass.ts +122 -196
  228. package/src/record/typed-frame-graph.ts +153 -9
  229. package/src/recovery/render-system-candidate.ts +22 -10
  230. package/src/render-contract.ts +23 -4
  231. package/src/render-pipeline.ts +6 -1
  232. package/src/render-system-extract-tail.ts +16 -0
  233. package/src/render-system-extract.ts +45 -13
  234. package/src/render-system.ts +255 -120
  235. package/src/scene/render-scene.ts +85 -4
  236. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  237. package/src/temporal/frame-coordinator.ts +8 -1
  238. package/src/temporal/frame.ts +20 -0
  239. package/src/temporal/view.ts +13 -2
  240. package/src/typed-render-graph-primitives.ts +121 -48
  241. package/src/volume/component.ts +0 -1
  242. package/src/volume/passes.ts +8 -4
  243. package/src/volume/resources.ts +1 -1
  244. package/src/volume/temporal.ts +11 -0
  245. package/dist/chunk-4NNZQCL3.mjs.map +0 -1
  246. package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
  247. package/dist/chunk-DRR7D3KX.mjs.map +0 -1
  248. package/dist/chunk-GWCEMDVR.mjs.map +0 -1
  249. package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
  250. package/dist/chunk-JRIPCMYC.mjs.map +0 -1
@@ -22,6 +22,7 @@ import type {
22
22
  import { RhiError } from '@forgeax/engine-rhi';
23
23
  import { err, ok, type Result } from '@forgeax/engine-types';
24
24
  import { executeRendererFrameTransaction } from '../assembly/renderer-frame-transaction';
25
+ import { standardBloomAdmitted } from '../bloom-admission';
25
26
  import { type RenderError, RenderFeatureStageFailedError } from '../errors/render';
26
27
  import { depthOfFieldTopology } from '../features/depth-of-field/depth-of-field-feature';
27
28
  import { resolveDepthOfFieldFrameParams } from '../features/depth-of-field/depth-of-field-params';
@@ -30,6 +31,7 @@ import {
30
31
  hasSubmissionSensitiveFeatures,
31
32
  type RenderFeaturePlanExecution,
32
33
  } from '../features/host';
34
+ import { motionBlurTemporalDemand } from '../features/motion-blur/motion-blur-params';
33
35
  import {
34
36
  type RenderFeaturePlannedFrame,
35
37
  renderFeaturePlanSignature,
@@ -62,7 +64,11 @@ import {
62
64
  } from '../gpu-texture-usage';
63
65
  import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_UNIFORM } from '../gpu-usage';
64
66
  import { createStandardSurfaceLightingBindGroup } from '../hdrp-buffers';
65
- import type { RenderFeatureGraphInspection } from '../inspection-types';
67
+ import type {
68
+ MotionBlurExecutionPass,
69
+ MotionBlurExecutionReceipt,
70
+ RenderFeatureGraphInspection,
71
+ } from '../inspection-types';
66
72
  import type { RenderExtent } from '../pipeline/render-extent';
67
73
  import {
68
74
  type StandardTopologyInputValue,
@@ -435,7 +441,15 @@ export function shouldUseTemporalFrameTransaction(
435
441
  export function isMotionBlurTemporalDemand(
436
442
  motionBlur: CameraSnapshot['motionBlur'] | undefined,
437
443
  ): boolean {
438
- return motionBlur?.shutterAngle !== undefined && motionBlur.shutterAngle > 0;
444
+ return (
445
+ motionBlur !== undefined &&
446
+ motionBlurTemporalDemand({
447
+ shutterAngle: motionBlur.shutterAngle,
448
+ maxRadiusPixels: motionBlur.maxRadiusPixels,
449
+ sampleCount: motionBlur.sampleCount,
450
+ targetFps: motionBlur.targetFps ?? 60,
451
+ })
452
+ );
439
453
  }
440
454
 
441
455
  /**
@@ -1364,6 +1378,7 @@ function topologyOf(
1364
1378
  standardLighting: StandardTopologyInputValue | undefined,
1365
1379
  ssr: SsrSpatialAdmission | undefined,
1366
1380
  renderExtent: RenderExtent | undefined,
1381
+ analyticFog: boolean,
1367
1382
  ): Result<RenderPipelineTopology, RhiError> {
1368
1383
  const cascadeCount = Math.max(1, Math.min(4, Math.round(lights.cascadeCount ?? 1))) as
1369
1384
  | 1
@@ -1430,6 +1445,7 @@ function topologyOf(
1430
1445
  reflectionFallback: { enabled: frameState.reflectionFallbackDemand === true },
1431
1446
  ssr,
1432
1447
  ...(renderExtent === undefined ? {} : { extent: renderExtent }),
1448
+ analyticFog: !clearOnly && analyticFog,
1433
1449
  surface: {
1434
1450
  width,
1435
1451
  height,
@@ -1440,7 +1456,11 @@ function topologyOf(
1440
1456
  camera: {
1441
1457
  tonemap: camera.tonemap,
1442
1458
  antialias: camera.antialias,
1443
- bloom: camera.bloom,
1459
+ // Bloom intensity is a frame parameter, not a graph identity. Fold
1460
+ // only its zero/non-zero admission into the closed topology switch so
1461
+ // positive intensity changes update the recorder UBO in place.
1462
+ bloom: standardBloomAdmitted(camera) ? 'on' : 'off',
1463
+ bloomIntensity: camera.bloomIntensity,
1444
1464
  ...(depthOfField === undefined ? {} : { depthOfField }),
1445
1465
  },
1446
1466
  atmosphere: !clearOnly && frameState.environmentFrame?.source.kind === 'atmosphere',
@@ -1839,6 +1859,7 @@ export function ensureCompiledFrameGraph(
1839
1859
  onRecoveryError?: (error: unknown) => void,
1840
1860
  ssr?: SsrSpatialAdmission,
1841
1861
  renderExtent?: RenderExtent,
1862
+ analyticFog = false,
1842
1863
  ): CompiledRenderGraph<RenderPipelineFrame> | null {
1843
1864
  const featureMetrics = mutableFeatureGraphInspection(internals);
1844
1865
  featureMetrics.ensureCalls += 1;
@@ -1904,6 +1925,7 @@ export function ensureCompiledFrameGraph(
1904
1925
  standardLighting,
1905
1926
  ssr,
1906
1927
  renderExtent,
1928
+ analyticFog,
1907
1929
  );
1908
1930
  if (!topologyResult.ok) {
1909
1931
  featureMetrics.buildFailures += 1;
@@ -1918,6 +1940,16 @@ export function ensureCompiledFrameGraph(
1918
1940
  const featurePostProcessEntries =
1919
1941
  featureGraphCandidate?.fullscreenEffects ??
1920
1942
  getRenderFeatureGraphState(internals).fullscreenEffects;
1943
+ // Motion Blur's compute plan intentionally has no raster fullscreen shader
1944
+ // declaration, but it is still a post chain consumer: Standard Post owns
1945
+ // the named `motion-input`/`motion-output` targets and must project it after
1946
+ // TAA. Keep the identity in this deferred set alongside declared fullscreen
1947
+ // effects so an early forward/deferred contribution cannot resolve its
1948
+ // scene-data target against an incomplete target map.
1949
+ const fullscreenFeatureIds = new Set([
1950
+ ...featurePostProcessEntries.keys(),
1951
+ 'forgeax.motion-blur',
1952
+ ]);
1921
1953
  const postProcessAdmission = resolvePostProcessChainAdmission(
1922
1954
  requestedPostEffects,
1923
1955
  (identity) => {
@@ -1965,8 +1997,19 @@ export function ensureCompiledFrameGraph(
1965
1997
  },
1966
1998
  }
1967
1999
  : topologyResult.value;
2000
+ // Keep Bloom's positive parameter values out of the compiled identity. A
2001
+ // crossing of zero has already changed topology.camera.bloom above; the
2002
+ // remaining values are uploaded by the active recorder without rebuilding
2003
+ // targets or pipelines.
2004
+ const topologyIdentity = {
2005
+ ...topology,
2006
+ camera: {
2007
+ ...topology.camera,
2008
+ bloomIntensity: undefined,
2009
+ },
2010
+ };
1968
2011
  const key = JSON.stringify({
1969
- topology,
2012
+ topology: topologyIdentity,
1970
2013
  // Observation domains are an explicit per-frame graph demand. They are
1971
2014
  // part of the cache identity so a request made after the ordinary graph
1972
2015
  // was compiled can admit the matching tone/output capture boundaries on
@@ -2087,7 +2130,6 @@ export function ensureCompiledFrameGraph(
2087
2130
  return lastKnownGood();
2088
2131
  }
2089
2132
  const ssrHistory = ssr?.status === 'admitted' ? importSsrHistoryTargets(builder) : undefined;
2090
- let projectedFeatures = false;
2091
2133
  const featureProjection = createRenderFeatureProjectionState();
2092
2134
  let featureShadows:
2093
2135
  | ReturnType<typeof projectRenderFeatureShadows<RenderPipelineFrame>>
@@ -2157,11 +2199,20 @@ export function ensureCompiledFrameGraph(
2157
2199
  namedTargets = {},
2158
2200
  standardSurfaceAccesses = [],
2159
2201
  ) => {
2160
- if (projectedFeatures) return ok(undefined);
2161
- projectedFeatures = true;
2162
- if (projectedFeaturePlans.value.length === 0) return ok(undefined);
2202
+ // Forward/deferred scene lanes contribute feature plans before the
2203
+ // Standard post chain has created its named input/output targets.
2204
+ // Fullscreen effects (Motion Blur included) must wait for that later
2205
+ // call so their temporal input is resolved after TAA. Other feature
2206
+ // plans can still be projected at the forward contribution point.
2207
+ const plans =
2208
+ Object.keys(namedTargets).length === 0
2209
+ ? projectedFeaturePlans.value.filter(
2210
+ (execution) => !fullscreenFeatureIds.has(execution.featureIdentity),
2211
+ )
2212
+ : projectedFeaturePlans.value;
2213
+ if (plans.length === 0) return ok(undefined);
2163
2214
  const resolveTarget = targetResolver(targets, semanticTargets, namedTargets);
2164
- const projected = projectPlanExecutions(builder, projectedFeaturePlans.value, {
2215
+ const projected = projectPlanExecutions(builder, plans, {
2165
2216
  state: featureProjection,
2166
2217
  resolveTarget,
2167
2218
  standardSurfaceAccesses,
@@ -2302,6 +2353,93 @@ export function ensureCompiledFrameGraph(
2302
2353
  return compiled.value;
2303
2354
  }
2304
2355
 
2356
+ /**
2357
+ * Project one accepted Motion Blur compute execution from the same graph and
2358
+ * resolved dispatch objects used by the encoder. A plan/capability candidate
2359
+ * without both executed passes deliberately returns no receipt.
2360
+ */
2361
+ function deriveMotionBlurExecutionReceipt(
2362
+ internals: RenderSystemInternals,
2363
+ frameState: RenderFrameState,
2364
+ ): MotionBlurExecutionReceipt | undefined {
2365
+ const graph = frameState.compiledFrameGraph;
2366
+ const deviceGeneration = internals.deviceScope?.generation;
2367
+ if (graph === null || deviceGeneration === undefined) return undefined;
2368
+ const info = graph.inspect();
2369
+ const graphPasses = new Map(info.passes.map((pass) => [pass.name, pass]));
2370
+ const summaryGraphPass = graphPasses.get('motion-blur-tile-summary');
2371
+ const reconstructGraphPass = graphPasses.get('motion-blur');
2372
+ if (summaryGraphPass?.kind !== 'compute' || reconstructGraphPass?.kind !== 'compute') {
2373
+ return undefined;
2374
+ }
2375
+ const planned = getRenderFeatureGraphState(internals).plans.find(
2376
+ (candidate) => candidate.featureIdentity === 'forgeax.motion-blur',
2377
+ );
2378
+ const execution =
2379
+ planned === undefined ? undefined : getRenderFeaturePlanExecutionProjection(planned);
2380
+ if (execution === undefined || execution.featureIdentity !== 'forgeax.motion-blur') {
2381
+ return undefined;
2382
+ }
2383
+ const executionPass = (name: 'motion-blur-tile-summary' | 'motion-blur') =>
2384
+ execution.passes.find((pass) => pass.name === name);
2385
+ const summary = executionPass('motion-blur-tile-summary');
2386
+ const reconstruct = executionPass('motion-blur');
2387
+ if (
2388
+ summary?.gpuCompute === undefined ||
2389
+ reconstruct?.gpuCompute === undefined ||
2390
+ summary.resolvedGpuCompute === undefined ||
2391
+ reconstruct.resolvedGpuCompute === undefined
2392
+ ) {
2393
+ return undefined;
2394
+ }
2395
+ const directWorkgroups = (
2396
+ pass: NonNullable<typeof summary.resolvedGpuCompute>,
2397
+ ): readonly [number, number, number] | undefined => {
2398
+ const dispatch = pass.dispatches[0];
2399
+ if (dispatch === undefined || !('workgroups' in dispatch)) return undefined;
2400
+ return dispatch.workgroups;
2401
+ };
2402
+ const summaryWorkgroups = directWorkgroups(summary.resolvedGpuCompute);
2403
+ const reconstructWorkgroups = directWorkgroups(reconstruct.resolvedGpuCompute);
2404
+ if (summaryWorkgroups === undefined || reconstructWorkgroups === undefined) return undefined;
2405
+ const outputResource = info.resources.find(
2406
+ (resource) =>
2407
+ resource.label === 'motion-blurred-color' ||
2408
+ reconstructGraphPass.accesses.some(
2409
+ (access) => access.resource === resource.label && access.usage === 'storage-write',
2410
+ ),
2411
+ );
2412
+ if (
2413
+ outputResource?.descriptor.kind !== 'texture' ||
2414
+ outputResource.descriptor.format !== 'rgba16float'
2415
+ ) {
2416
+ return undefined;
2417
+ }
2418
+ const summaryReceipt: MotionBlurExecutionPass = {
2419
+ name: 'motion-blur-tile-summary',
2420
+ kind: 'compute',
2421
+ entryPoint: 'tile_summary',
2422
+ workgroups: summaryWorkgroups,
2423
+ };
2424
+ const reconstructReceipt: MotionBlurExecutionPass = {
2425
+ name: 'motion-blur',
2426
+ kind: 'compute',
2427
+ entryPoint: 'reconstruct',
2428
+ workgroups: reconstructWorkgroups,
2429
+ };
2430
+ return Object.freeze({
2431
+ frameId: frameState.frameNumber,
2432
+ deviceGeneration,
2433
+ // RenderGraphBuilder generations are process-global. The renderer's
2434
+ // frame-state generation is the identity used by inspection and by the
2435
+ // same-frame admission check below, so retain that owner-scoped value.
2436
+ graphGeneration: frameState.graphGeneration,
2437
+ program: 'motion-blur-compute' as const,
2438
+ outputFormat: 'rgba16float' as const,
2439
+ passes: [summaryReceipt, reconstructReceipt] as const,
2440
+ });
2441
+ }
2442
+
2305
2443
  export function executeCompiledFrameGraph(
2306
2444
  internals: RenderSystemInternals,
2307
2445
  frameState: RenderFrameState,
@@ -2338,6 +2476,7 @@ export function executeCompiledFrameGraph(
2338
2476
  rejectTemporalFrame();
2339
2477
  return false;
2340
2478
  }
2479
+ const motionBlurExecution = deriveMotionBlurExecutionReceipt(internals, frameState);
2341
2480
  const legacyTimingInstrumentation =
2342
2481
  timingCapture === undefined ? undefined : createTimingInstrumentation(timingCapture);
2343
2482
  const timingHost = internals as RenderSystemInternals & {
@@ -2960,6 +3099,11 @@ export function executeCompiledFrameGraph(
2960
3099
  commit: () => {
2961
3100
  resolvedFrameHooks?.onSubmitted?.();
2962
3101
  legacySubmitCommit?.();
3102
+ if (motionBlurExecution === undefined) {
3103
+ delete frameState.motionBlurExecution;
3104
+ } else {
3105
+ frameState.motionBlurExecution = motionBlurExecution;
3106
+ }
2963
3107
  if (frameState.temporalFrameInput !== undefined) {
2964
3108
  const temporal = frameState.temporalFrameTransaction.commit({ accepted: true });
2965
3109
  if (temporal.ok) {
@@ -11,6 +11,7 @@ import {
11
11
  } from '@forgeax/engine-rhi';
12
12
  import type { Handle, MeshAsset, SamplerAsset, TextureAsset } from '@forgeax/engine-types';
13
13
  import { toShared } from '@forgeax/engine-types';
14
+ import { standardBloomAdmitted } from '../bloom-admission';
14
15
  import { createClusterBinScratch } from '../cluster-binner';
15
16
  import type { EnvironmentLifecycle } from '../environment/lifecycle';
16
17
  import {
@@ -19,6 +20,7 @@ import {
19
20
  type RenderFeaturePreparedGraphicsResolverInput,
20
21
  runRenderFeatureFrame,
21
22
  } from '../features/host';
23
+ import { motionBlurTemporalDemand } from '../features/motion-blur/motion-blur-params';
22
24
  import type { RenderFeatureGpuWorkOwner } from '../features/prepared-gpu-work';
23
25
  import { resolveStandardRenderFeatureTargets } from '../features/targets';
24
26
  import {
@@ -532,6 +534,10 @@ export function createRenderSystemRecovery(
532
534
  worlds: seed.worlds,
533
535
  owner: seed.resourceOwner,
534
536
  frameNumber: candidateFrameState.frameNumber,
537
+ frameSize: {
538
+ width: runtime.internals.canvas.width,
539
+ height: runtime.internals.canvas.height,
540
+ },
535
541
  visibilitySnapshots: seed.frame.featureVisibilitySnapshots,
536
542
  hiddenEntityReports: seed.frame.hiddenEntityReports,
537
543
  targets: featureTargets,
@@ -794,29 +800,35 @@ export function createRenderSystemRecovery(
794
800
  throw new Error('recovery TAA pipeline is unavailable');
795
801
  }
796
802
  }
797
- if (camera.motionBlur?.shutterAngle !== undefined && camera.motionBlur.shutterAngle > 0) {
803
+ if (
804
+ camera.motionBlur !== undefined &&
805
+ motionBlurTemporalDemand({
806
+ shutterAngle: camera.motionBlur.shutterAngle,
807
+ maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
808
+ sampleCount: camera.motionBlur.sampleCount,
809
+ targetFps: camera.motionBlur.targetFps ?? 60,
810
+ })
811
+ ) {
798
812
  warmPostProcessPipeline('forgeax.motion-blur', 'rgba16float');
799
813
  }
800
814
  for (const id of candidatePostProcessIds) {
801
815
  warmPostProcessPipeline(id, runtime.pipelineState.format as GPUTextureFormat);
802
816
  }
803
- if (camera.bloom === 'on' && camera.tonemap !== 'none') {
817
+ if (standardBloomAdmitted(camera)) {
804
818
  runtime.pipelineState.perPassResources.ensureBloomResources?.();
805
819
  const bloom = runtime.pipelineState.perPassResources.getBloomResources?.();
806
820
  if (
807
821
  bloom === null ||
808
822
  bloom === undefined ||
809
- bloom.bloomBrightPipeline === null ||
810
- bloom.bloomBlurHPipeline === null ||
811
- bloom.bloomBlurVPipeline === null ||
823
+ bloom.bloomDownsamplePipeline === null ||
824
+ bloom.bloomUpsamplePipeline === null ||
812
825
  bloom.bloomCompositePipeline === null ||
813
- bloom.bloomBrightBindGroupLayout === null ||
814
- bloom.bloomBlurBindGroupLayout === null ||
826
+ bloom.bloomDownsampleBindGroupLayout === null ||
827
+ bloom.bloomUpsampleBindGroupLayout === null ||
815
828
  bloom.bloomCompositeBindGroupLayout === null ||
816
829
  bloom.bloomSampler === null ||
817
- bloom.bloomBrightParamsBuffer === null ||
818
- bloom.bloomBlurHParamsBuffer === null ||
819
- bloom.bloomBlurVParamsBuffer === null ||
830
+ bloom.bloomDownsampleParamsBuffer === null ||
831
+ bloom.bloomUpsampleParamsBuffer === null ||
820
832
  bloom.bloomCompositeParamsBuffer === null
821
833
  ) {
822
834
  throw new Error('recovery Bloom resources are incomplete');
@@ -170,7 +170,8 @@ export interface CameraSnapshot {
170
170
  readonly bloom: BloomEnabled;
171
171
  readonly bloomThreshold: number;
172
172
  readonly bloomIntensity: number;
173
- readonly bloomBlurRadius: number;
173
+ readonly bloomSoftKnee: number;
174
+ readonly bloomScatter: number;
174
175
  readonly clearColor: readonly [number, number, number, number];
175
176
  /** Optional active-camera SSR authoring fact, copied from the ECS schema. */
176
177
  readonly screenSpaceReflection?: ScreenSpaceReflectionData;
@@ -185,6 +186,8 @@ export interface CameraSnapshot {
185
186
  readonly shutterAngle: number;
186
187
  readonly maxRadiusPixels: number;
187
188
  readonly sampleCount: number;
189
+ /** Target presentation rate; zero follows the accepted frame interval. */
190
+ readonly targetFps?: number;
188
191
  };
189
192
  }
190
193
 
@@ -566,6 +569,19 @@ export interface RenderFrameInput {
566
569
  readonly leases: readonly RenderWorldLease[];
567
570
  readonly camera: FrameCamera;
568
571
  readonly environment: FrameEnvironment;
572
+ /**
573
+ * Monotonic host render-sample time in seconds. This is captured before ECS
574
+ * applies its simulation delta clamp, then accepted with frame submission.
575
+ * Omitting it keeps direct renderer callers on the ECS-clock fallback.
576
+ */
577
+ readonly sampleTimeSeconds?: number;
578
+ /**
579
+ * Mark the next submitted frame as a fresh temporal baseline. App sets this
580
+ * on resume so a short wall-clock pause cannot be mistaken for a valid
581
+ * motion pair; direct replay callers may use the same explicit lifecycle
582
+ * boundary.
583
+ */
584
+ readonly temporalReset?: boolean;
569
585
  /** Fixed-step publication consumed by dynamic geometry candidates. */
570
586
  readonly fixedStep?: number;
571
587
  /** Optional profiler correlation token owned by App and consumed by Render. */
@@ -811,6 +827,10 @@ export type DrawOwnerOptions = {
811
827
  readonly resourceOwner: number;
812
828
  /** Optional FrameCamera entity selection; absent preserves ActiveCamera semantics. */
813
829
  readonly cameraEntityKey?: number;
830
+ /** Monotonic host render-sample time in seconds, before the ECS delta clamp. */
831
+ readonly sampleTimeSeconds?: number;
832
+ /** Explicit pause/resume or replay lifecycle boundary for temporal owners. */
833
+ readonly temporalReset?: boolean;
814
834
  readonly profileFrame?: ProfileFrameToken;
815
835
  };
816
836
 
@@ -909,9 +929,8 @@ export const RENDER_GRAPH_EXECUTION_PHASE_CATALOG = [
909
929
  'record/graph-execute/skybox',
910
930
  'record/graph-execute/main',
911
931
  'record/graph-execute/fxaa',
912
- 'record/graph-execute/bloom-bright',
913
- 'record/graph-execute/bloom-blur-h',
914
- 'record/graph-execute/bloom-blur-v',
932
+ 'record/graph-execute/bloom-downsample',
933
+ 'record/graph-execute/bloom-upsample',
915
934
  'record/graph-execute/bloom-composite',
916
935
  'record/graph-execute/ssr-hiz',
917
936
  'record/graph-execute/ssr-trace',
@@ -224,6 +224,8 @@ export interface RenderPipelineTopology {
224
224
  readonly clearOnly?: boolean | undefined;
225
225
  /** Selected atmosphere requires the graph-owned sky producer and background. */
226
226
  readonly atmosphere?: boolean | undefined;
227
+ /** Finite-depth distance / exponential-height fog; no volume history. */
228
+ readonly analyticFog?: boolean | undefined;
227
229
  /** Whether Standard main draws carry the producer-owned fallback MRT. */
228
230
  readonly reflectionFallback?: { readonly enabled: boolean } | undefined;
229
231
  readonly surface: {
@@ -235,7 +237,10 @@ export interface RenderPipelineTopology {
235
237
  };
236
238
  /** Renderer-owned immutable dimensions for Standard graph consumers. */
237
239
  readonly extent?: RenderExtent;
238
- readonly camera: Pick<RenderPipelineContext['camera'], 'tonemap' | 'antialias' | 'bloom'> & {
240
+ readonly camera: Pick<
241
+ RenderPipelineContext['camera'],
242
+ 'tonemap' | 'antialias' | 'bloom' | 'bloomIntensity'
243
+ > & {
239
244
  /** Shape-only DoF admission; optical values remain per-frame UBO data. */
240
245
  readonly depthOfField?:
241
246
  | {
@@ -86,6 +86,7 @@ import {
86
86
  gpuDrivenSourceDrawItemIndex,
87
87
  } from './extract/gpu-driven';
88
88
  import type { VisibilitySnapshot } from './extract/visibility';
89
+ import { MotionBlurValidationError } from './features/motion-blur/motion-blur-params';
89
90
  import type {
90
91
  RenderFeatureHiddenEntityReport,
91
92
  RenderFeatureWorldVisibilitySnapshot,
@@ -350,6 +351,11 @@ export function extractFrames(
350
351
 
351
352
  framesByWorld.set(wi, frame);
352
353
  } catch (err) {
354
+ // Camera effect parameters are part of the public render contract. Keep
355
+ // their typed validation error on the renderer path so draw() can return
356
+ // a RenderError and publish the invalid inspection state; ordinary
357
+ // per-world extraction failures retain the existing world-local route.
358
+ if (err instanceof MotionBlurValidationError) throw err;
353
359
  // Per-world failure: route to world's own error handler, skip contribution.
354
360
  try {
355
361
  createWorldInternalView(world)._routeError(err, {
@@ -1571,6 +1577,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1571
1577
  mieCoefficient: value.mieCoefficient,
1572
1578
  mieDirectionalG: value.mieDirectionalG,
1573
1579
  sunAngularRadius: value.sunAngularRadius,
1580
+ circumsolarStrength: value.circumsolarStrength,
1581
+ circumsolarWidth: value.circumsolarWidth,
1574
1582
  } as const;
1575
1583
  environmentCandidates.push({
1576
1584
  kind: 'atmosphere',
@@ -1881,6 +1889,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1881
1889
  if (!authorVisible && !retainHidden) {
1882
1890
  continue;
1883
1891
  }
1892
+ // A scalar shared<T> field uses slot 0 as the schema-level unbound
1893
+ // sentinel. An entity carrying an unbound MeshFilter is not a
1894
+ // renderable yet; keep it out of frame validation. Non-zero handles
1895
+ // still follow the normal resolve/error path below.
1896
+ if (isRenderable && fAssetHandle === 0) {
1897
+ continue;
1898
+ }
1884
1899
  const layerVal = fLayerValue ?? 0;
1885
1900
  // bug-20260709-builtin-quad-withoutaabb-disables-sprite-frustum-cu M2.5
1886
1901
  // (carries PR #598 feat-20260703 D-7): dispatch entries for this entity
@@ -3091,6 +3106,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3091
3106
  }
3092
3107
  instancesSnap = {
3093
3108
  transforms: collectionResult.value.transforms,
3109
+ generations: collectionResult.value.generations,
3094
3110
  instanceCount: collectionResult.value.count,
3095
3111
  collectionId: collectionResult.value.collectionId,
3096
3112
  revision: collectionResult.value.revision,
@@ -27,7 +27,7 @@ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-c
27
27
  // fire; no renderable; no dispatch.
28
28
  // - case B (material === 0) : missing-spec sentinel; defaultMaterial
29
29
  // Snapshot fills the slot. No fire.
30
- // - case C (handle unresolved): assets.get(handle).err with the entity
30
+ // - case C (non-zero handle unresolved): assets.get(handle).err with the entity
31
31
  // carrying the full T+MF+MR renderable
32
32
  // archetype -> structured RhiError(
33
33
  // `asset-not-registered`) routed through
@@ -61,8 +61,8 @@ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-c
61
61
  // material; hint literal: `'pass
62
62
  // undefined or omit field to request
63
63
  // default material'` (AC-10).
64
- // - case C dangling-ref : `assets.get(handle).err` (strict path,
65
- // isRenderable === true) ->
64
+ // - case C dangling-ref : `assets.get(handle).err` for a non-zero
65
+ // handle (strict path, isRenderable === true) ->
66
66
  // `RhiError({ code: 'asset-not-
67
67
  // registered', detail: { assetHandle },
68
68
  // hint: 'register material via
@@ -73,7 +73,9 @@ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-c
73
73
  // entity skipped from
74
74
  // RenderableSnapshot[] +
75
75
  // MaterialDispatchSnapshot[]; mirrors
76
- // `MeshFilter.assetHandle` dangling path
76
+ // `MeshFilter.assetHandle` dangling path. A
77
+ // scalar shared<T> slot 0 is the unbound,
78
+ // non-renderable state and is skipped
77
79
  // (charter proposition 5; AC-11).
78
80
  //
79
81
  // Renderer-owned Large Instances:
@@ -96,7 +98,14 @@ import {
96
98
  selectMaterialPassProgram,
97
99
  walkMaterialPassesOverSharedRefs,
98
100
  } from '@forgeax/engine-assets-runtime';
99
- import type { Component, ComponentSchema, EntityHandle, Query, World } from '@forgeax/engine-ecs';
101
+ import {
102
+ type Component,
103
+ type ComponentSchema,
104
+ type EntityHandle,
105
+ type Query,
106
+ Time,
107
+ type World,
108
+ } from '@forgeax/engine-ecs';
100
109
  import { readRenderArrayView, routeWorldError } from '@forgeax/engine-ecs/projection';
101
110
  import { type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
102
111
  import { AssetGuid, type AssetGuid as AssetGuidBytes } from '@forgeax/engine-pack/guid';
@@ -133,7 +142,6 @@ import {
133
142
  import { buildCubeCameraFaceViews } from './capture/cube-views';
134
143
  import {
135
144
  antialiasFromF32,
136
- bloomEnabledFromF32,
137
145
  CAMERA_EXPOSURE_MODE_MANUAL,
138
146
  Camera,
139
147
  CubeCamera,
@@ -148,6 +156,7 @@ import {
148
156
  ScreenSpaceReflection,
149
157
  tonemapFromF32,
150
158
  tonemapToU32,
159
+ validateCameraBloom,
151
160
  validateDynamicResolutionCamera,
152
161
  } from './components';
153
162
  import { DepthOfField } from './components/depth-of-field';
@@ -171,6 +180,7 @@ import {
171
180
  depthOfFieldRequestFailure,
172
181
  resolveDepthOfFieldParams,
173
182
  } from './features/depth-of-field/depth-of-field-params';
183
+ import { resolveMotionBlurParams } from './features/motion-blur/motion-blur-params';
174
184
  import type {
175
185
  RenderFeatureHiddenEntityReport,
176
186
  RenderFeatureWorldVisibilitySnapshot,
@@ -665,6 +675,8 @@ export interface ExtractedVolumetricFog {
665
675
  readonly generation?: number;
666
676
  readonly digest?: string;
667
677
  readonly worldId?: number;
678
+ /** Canonical simulation time projected from this World for authored medium motion. */
679
+ readonly worldTimeSeconds?: number;
668
680
  /** Required same-World light selected by the authoring component. */
669
681
  readonly lightEntity?: EntityHandle;
670
682
  readonly lightKind?: 'directional' | 'point' | 'spot';
@@ -816,6 +828,9 @@ export type RenderableReactiveReason =
816
828
  export interface RenderableTemporalSnapshot {
817
829
  readonly previousEpoch: number | undefined;
818
830
  readonly previousSource: RenderablePreviousSource;
831
+ /** True only when current and previous geometry belong to one stable source identity. */
832
+ readonly motionValid: boolean;
833
+ /** Color-history invalidation is independent from motion validity. */
819
834
  readonly reactive: boolean;
820
835
  readonly reactiveReasons: readonly RenderableReactiveReason[];
821
836
  readonly previousTransform: TransformSnapshot;
@@ -836,6 +851,8 @@ export interface InstancesSnapshot {
836
851
  readonly collectionId?: InstanceCollectionId;
837
852
  /** Packed column-major mat4 transforms (16 f32 per instance). */
838
853
  readonly transforms: Float32Array;
854
+ /** Monotonic per-ordinal identity generations, when the collection owns them. */
855
+ readonly generations?: Uint32Array;
839
856
  /** Number of instances (transforms.length / 16). */
840
857
  readonly instanceCount: number;
841
858
  /** Collection revision captured by this detached frame snapshot. */
@@ -2994,6 +3011,9 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
2994
3011
  for (const row of cameraQuery) {
2995
3012
  const cam = row.get(Camera);
2996
3013
  const motionBlur = row.get(MotionBlur);
3014
+ const motionBlurResult = resolveMotionBlurParams(motionBlur);
3015
+ if (!motionBlurResult.ok) throw motionBlurResult.error;
3016
+ const motionBlurParams = motionBlurResult.value;
2997
3017
  const depthOfField = row.get(DepthOfField);
2998
3018
  const dynamicResolution = row.get(DynamicResolution);
2999
3019
  const screenSpaceReflection = row.get(ScreenSpaceReflection);
@@ -3054,10 +3074,17 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
3054
3074
  ...(dynamicResolutionResult.value === undefined
3055
3075
  ? {}
3056
3076
  : { dynamicResolution: dynamicResolutionResult.value }),
3057
- bloom: bloomEnabledFromF32(cam.bloom),
3077
+ bloom: validateCameraBloom(
3078
+ cam.bloom,
3079
+ cam.bloomThreshold,
3080
+ cam.bloomIntensity,
3081
+ cam.bloomSoftKnee,
3082
+ cam.bloomScatter,
3083
+ ),
3058
3084
  bloomThreshold: cam.bloomThreshold,
3059
3085
  bloomIntensity: cam.bloomIntensity,
3060
- bloomBlurRadius: cam.bloomBlurRadius,
3086
+ bloomSoftKnee: cam.bloomSoftKnee,
3087
+ bloomScatter: cam.bloomScatter,
3061
3088
  clearColor: [
3062
3089
  cam.clearColor[0] ?? 0,
3063
3090
  cam.clearColor[1] ?? 0,
@@ -3068,13 +3095,14 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
3068
3095
  ? { depthOfField: depthOfFieldResult.value }
3069
3096
  : {}),
3070
3097
  ...(depthOfFieldError === undefined ? {} : { depthOfFieldError }),
3071
- ...(motionBlur === undefined
3098
+ ...(motionBlurParams === undefined
3072
3099
  ? {}
3073
3100
  : {
3074
3101
  motionBlur: {
3075
- shutterAngle: motionBlur.shutterAngle,
3076
- maxRadiusPixels: motionBlur.maxRadiusPixels,
3077
- sampleCount: motionBlur.sampleCount,
3102
+ shutterAngle: motionBlurParams.shutterAngle,
3103
+ maxRadiusPixels: motionBlurParams.maxRadiusPixels,
3104
+ sampleCount: motionBlurParams.sampleCount,
3105
+ targetFps: motionBlurParams.targetFps,
3078
3106
  },
3079
3107
  }),
3080
3108
  ...(screenSpaceReflection === undefined
@@ -3332,6 +3360,8 @@ export function extractVolumeSnapshot(
3332
3360
  assets: AssetRegistry | null | undefined,
3333
3361
  worldInternal: WorldInternalView,
3334
3362
  ): ExtractedVolumetricFog | undefined {
3363
+ const elapsed = world.getResource(Time).elapsed;
3364
+ const worldTimeSeconds = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
3335
3365
  const query = world.query({ read: [VolumetricFog] }).unwrap();
3336
3366
  const authorings: VolumetricFogAuthoring[] = [];
3337
3367
  let selectedLight:
@@ -3377,7 +3407,7 @@ export function extractVolumeSnapshot(
3377
3407
  : resolved.error,
3378
3408
  { severity: Severity.Error, systemName: 'RenderSystem.extract (volumetric-density)' },
3379
3409
  );
3380
- return { status: 'degraded' };
3410
+ return { status: 'degraded', worldTimeSeconds };
3381
3411
  }
3382
3412
  const densityAsset = resolved.value;
3383
3413
  const guid = assets?.guidOf(densityAsset) ?? `handle:${Number(densityHandle)}`;
@@ -3441,6 +3471,7 @@ export function extractVolumeSnapshot(
3441
3471
  });
3442
3472
  return {
3443
3473
  status: 'degraded',
3474
+ worldTimeSeconds,
3444
3475
  ...(first === undefined
3445
3476
  ? {}
3446
3477
  : {
@@ -3480,6 +3511,7 @@ export function extractVolumeSnapshot(
3480
3511
  };
3481
3512
  return {
3482
3513
  status: 'available',
3514
+ worldTimeSeconds,
3483
3515
  fog: extracted.value.fog,
3484
3516
  densityHandle: first.densityHandle,
3485
3517
  densityAsset: first.densityAsset,