@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
@@ -22,12 +22,16 @@ 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';
27
+ import { depthOfFieldTopology } from '../features/depth-of-field/depth-of-field-feature';
28
+ import { resolveDepthOfFieldFrameParams } from '../features/depth-of-field/depth-of-field-params';
26
29
  import {
27
30
  getRenderFeaturePlanExecutionProjection,
28
31
  hasSubmissionSensitiveFeatures,
29
32
  type RenderFeaturePlanExecution,
30
33
  } from '../features/host';
34
+ import { motionBlurTemporalDemand } from '../features/motion-blur/motion-blur-params';
31
35
  import {
32
36
  type RenderFeaturePlannedFrame,
33
37
  renderFeaturePlanSignature,
@@ -60,7 +64,11 @@ import {
60
64
  } from '../gpu-texture-usage';
61
65
  import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_UNIFORM } from '../gpu-usage';
62
66
  import { createStandardSurfaceLightingBindGroup } from '../hdrp-buffers';
63
- import type { RenderFeatureGraphInspection } from '../inspection-types';
67
+ import type {
68
+ MotionBlurExecutionPass,
69
+ MotionBlurExecutionReceipt,
70
+ RenderFeatureGraphInspection,
71
+ } from '../inspection-types';
64
72
  import type { RenderExtent } from '../pipeline/render-extent';
65
73
  import {
66
74
  type StandardTopologyInputValue,
@@ -433,7 +441,15 @@ export function shouldUseTemporalFrameTransaction(
433
441
  export function isMotionBlurTemporalDemand(
434
442
  motionBlur: CameraSnapshot['motionBlur'] | undefined,
435
443
  ): boolean {
436
- 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
+ );
437
453
  }
438
454
 
439
455
  /**
@@ -1362,6 +1378,7 @@ function topologyOf(
1362
1378
  standardLighting: StandardTopologyInputValue | undefined,
1363
1379
  ssr: SsrSpatialAdmission | undefined,
1364
1380
  renderExtent: RenderExtent | undefined,
1381
+ analyticFog: boolean,
1365
1382
  ): Result<RenderPipelineTopology, RhiError> {
1366
1383
  const cascadeCount = Math.max(1, Math.min(4, Math.round(lights.cascadeCount ?? 1))) as
1367
1384
  | 1
@@ -1419,6 +1436,7 @@ function topologyOf(
1419
1436
  : undefined;
1420
1437
  const preparedLut =
1421
1438
  frameState.pendingStandardLutGpuResources ?? frameState.standardLutGpuResources;
1439
+ const depthOfField = depthOfFieldTopology(camera.depthOfField);
1422
1440
  return ok({
1423
1441
  pipelineId: STANDARD_PIPELINE_ID,
1424
1442
  standardProfile: internals.standardProfile,
@@ -1427,6 +1445,7 @@ function topologyOf(
1427
1445
  reflectionFallback: { enabled: frameState.reflectionFallbackDemand === true },
1428
1446
  ssr,
1429
1447
  ...(renderExtent === undefined ? {} : { extent: renderExtent }),
1448
+ analyticFog: !clearOnly && analyticFog,
1430
1449
  surface: {
1431
1450
  width,
1432
1451
  height,
@@ -1437,7 +1456,12 @@ function topologyOf(
1437
1456
  camera: {
1438
1457
  tonemap: camera.tonemap,
1439
1458
  antialias: camera.antialias,
1440
- 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,
1464
+ ...(depthOfField === undefined ? {} : { depthOfField }),
1441
1465
  },
1442
1466
  atmosphere: !clearOnly && frameState.environmentFrame?.source.kind === 'atmosphere',
1443
1467
  output: {
@@ -1754,18 +1778,66 @@ export function settleVolumetricFogGraphCandidate(
1754
1778
  if (candidate === null) return;
1755
1779
  const previous = frameState.volumetricFogPreviousGraph ?? null;
1756
1780
  const previousKey = frameState.volumetricFogPreviousGraphKey ?? null;
1781
+ // A frame can replace both the volume and DoF topology at once. DoF owns
1782
+ // the shared compiled graph's full projection (per-frame resolver,
1783
+ // generation, and lighting signature), so let it settle the graph exactly
1784
+ // once while volume clears only its own candidate marker.
1785
+ const sharedWithDepthOfField = frameState.depthOfFieldCandidateGraph === candidate;
1757
1786
  if (submitted) {
1758
- if (previous !== null && previous !== candidate) retire(frameState, previous);
1787
+ if (!sharedWithDepthOfField && previous !== null && previous !== candidate) {
1788
+ retire(frameState, previous);
1789
+ }
1759
1790
  } else {
1760
- if (candidate !== previous) retire(frameState, candidate);
1761
- frameState.compiledFrameGraph = previous;
1762
- frameState.compiledFrameGraphTopologyKey = previousKey;
1791
+ if (!sharedWithDepthOfField) {
1792
+ if (candidate !== previous) retire(frameState, candidate);
1793
+ frameState.compiledFrameGraph = previous;
1794
+ frameState.compiledFrameGraphTopologyKey = previousKey;
1795
+ }
1763
1796
  }
1764
1797
  frameState.volumetricFogPreviousGraph = null;
1765
1798
  frameState.volumetricFogPreviousGraphKey = null;
1766
1799
  frameState.volumetricFogCandidateGraph = null;
1767
1800
  }
1768
1801
 
1802
+ /**
1803
+ * Settle a DoF graph replacement at the same submit barrier as the frame.
1804
+ * Graph compilation publishes a candidate for the current record pass, but a
1805
+ * failed encode/finish/submit must restore every graph-owned LKG projection
1806
+ * before the next frame can inspect or reuse it.
1807
+ */
1808
+ export function settleDepthOfFieldGraphCandidate(
1809
+ frameState: RenderFrameState,
1810
+ submitted: boolean,
1811
+ ): void {
1812
+ const candidate = frameState.depthOfFieldCandidateGraph;
1813
+ if (candidate === null || candidate === undefined) return;
1814
+ // Sparse synthetic frame states used by graph tests predate the explicit
1815
+ // transaction fields. Treat omitted pending values as the empty LKG rather
1816
+ // than passing undefined into retirement or restoring an invalid key.
1817
+ const previous = frameState.depthOfFieldPreviousGraph ?? null;
1818
+ if (submitted) {
1819
+ if (previous !== null && previous !== candidate) retire(frameState, previous);
1820
+ } else {
1821
+ if (candidate !== previous) retire(frameState, candidate);
1822
+ frameState.compiledFrameGraph = previous;
1823
+ frameState.compiledFrameGraphTopologyKey = frameState.depthOfFieldPreviousGraphKey ?? null;
1824
+ if (frameState.depthOfFieldPreviousPerFrameGraph === undefined) {
1825
+ delete frameState.perFrameGraph;
1826
+ } else {
1827
+ frameState.perFrameGraph = frameState.depthOfFieldPreviousPerFrameGraph;
1828
+ }
1829
+ frameState.graphGeneration = frameState.depthOfFieldPreviousGraphGeneration ?? 0;
1830
+ frameState.standardLightingGraphSignature =
1831
+ frameState.depthOfFieldPreviousStandardLightingGraphSignature ?? '';
1832
+ }
1833
+ frameState.depthOfFieldPreviousGraph = null;
1834
+ frameState.depthOfFieldPreviousGraphKey = null;
1835
+ frameState.depthOfFieldPreviousPerFrameGraph = undefined;
1836
+ frameState.depthOfFieldPreviousGraphGeneration = 0;
1837
+ frameState.depthOfFieldPreviousStandardLightingGraphSignature = '';
1838
+ frameState.depthOfFieldCandidateGraph = null;
1839
+ }
1840
+
1769
1841
  export function ensureCompiledFrameGraph(
1770
1842
  internals: RenderSystemInternals,
1771
1843
  frameState: RenderFrameState,
@@ -1787,10 +1859,23 @@ export function ensureCompiledFrameGraph(
1787
1859
  onRecoveryError?: (error: unknown) => void,
1788
1860
  ssr?: SsrSpatialAdmission,
1789
1861
  renderExtent?: RenderExtent,
1862
+ analyticFog = false,
1790
1863
  ): CompiledRenderGraph<RenderPipelineFrame> | null {
1791
1864
  const featureMetrics = mutableFeatureGraphInspection(internals);
1792
1865
  featureMetrics.ensureCalls += 1;
1793
1866
  const requestedStandardLightingSignature = standardLightingTopologySignature(standardLighting);
1867
+ const frameDepthOfField = resolveDepthOfFieldFrameParams(
1868
+ camera.depthOfField,
1869
+ camera.depthOfFieldError,
1870
+ frameState.depthOfFieldAccepted?.params,
1871
+ );
1872
+ // A rejected authoring request remains visible on the source camera, while
1873
+ // an accepted graph continues to receive its own params until a valid
1874
+ // replacement or explicit component removal is submitted.
1875
+ const graphCamera: CameraSnapshot =
1876
+ frameDepthOfField === undefined || frameDepthOfField === camera.depthOfField
1877
+ ? camera
1878
+ : { ...camera, depthOfField: frameDepthOfField };
1794
1879
  const lastKnownGood = (): CompiledRenderGraph<RenderPipelineFrame> | null =>
1795
1880
  frameState.compiledFrameGraph !== null &&
1796
1881
  !hasSubmissionSensitiveFeatures(getRenderFeatureGraphState(internals).plans) &&
@@ -1825,7 +1910,7 @@ export function ensureCompiledFrameGraph(
1825
1910
  internals,
1826
1911
  frameState,
1827
1912
  pipelineState,
1828
- camera,
1913
+ graphCamera,
1829
1914
  clearOnly,
1830
1915
  lights,
1831
1916
  surfaceWidth,
@@ -1840,6 +1925,7 @@ export function ensureCompiledFrameGraph(
1840
1925
  standardLighting,
1841
1926
  ssr,
1842
1927
  renderExtent,
1928
+ analyticFog,
1843
1929
  );
1844
1930
  if (!topologyResult.ok) {
1845
1931
  featureMetrics.buildFailures += 1;
@@ -1854,6 +1940,16 @@ export function ensureCompiledFrameGraph(
1854
1940
  const featurePostProcessEntries =
1855
1941
  featureGraphCandidate?.fullscreenEffects ??
1856
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
+ ]);
1857
1953
  const postProcessAdmission = resolvePostProcessChainAdmission(
1858
1954
  requestedPostEffects,
1859
1955
  (identity) => {
@@ -1901,8 +1997,19 @@ export function ensureCompiledFrameGraph(
1901
1997
  },
1902
1998
  }
1903
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
+ };
1904
2011
  const key = JSON.stringify({
1905
- topology,
2012
+ topology: topologyIdentity,
1906
2013
  // Observation domains are an explicit per-frame graph demand. They are
1907
2014
  // part of the cache identity so a request made after the ordinary graph
1908
2015
  // was compiled can admit the matching tone/output capture boundaries on
@@ -2023,7 +2130,6 @@ export function ensureCompiledFrameGraph(
2023
2130
  return lastKnownGood();
2024
2131
  }
2025
2132
  const ssrHistory = ssr?.status === 'admitted' ? importSsrHistoryTargets(builder) : undefined;
2026
- let projectedFeatures = false;
2027
2133
  const featureProjection = createRenderFeatureProjectionState();
2028
2134
  let featureShadows:
2029
2135
  | ReturnType<typeof projectRenderFeatureShadows<RenderPipelineFrame>>
@@ -2032,6 +2138,7 @@ export function ensureCompiledFrameGraph(
2032
2138
  const built = frameState.activePipeline.build(
2033
2139
  {
2034
2140
  graph: builder,
2141
+ camera: { depthOfField: graphCamera.depthOfField },
2035
2142
  observationCaptureDomains: internals.observationCaptureDomains,
2036
2143
  ...(autoExposureResources?.ok === true
2037
2144
  ? { standardOutput: { autoExposure: autoExposureResources.value } }
@@ -2092,11 +2199,20 @@ export function ensureCompiledFrameGraph(
2092
2199
  namedTargets = {},
2093
2200
  standardSurfaceAccesses = [],
2094
2201
  ) => {
2095
- if (projectedFeatures) return ok(undefined);
2096
- projectedFeatures = true;
2097
- 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);
2098
2214
  const resolveTarget = targetResolver(targets, semanticTargets, namedTargets);
2099
- const projected = projectPlanExecutions(builder, projectedFeaturePlans.value, {
2215
+ const projected = projectPlanExecutions(builder, plans, {
2100
2216
  state: featureProjection,
2101
2217
  resolveTarget,
2102
2218
  standardSurfaceAccesses,
@@ -2172,10 +2288,32 @@ export function ensureCompiledFrameGraph(
2172
2288
  return lastKnownGood();
2173
2289
  }
2174
2290
  const previous = frameState.compiledFrameGraph;
2291
+ const depthOfFieldReplacement =
2292
+ (frameState.depthOfFieldCandidateGraph === null ||
2293
+ frameState.depthOfFieldCandidateGraph === undefined) &&
2294
+ (topologyResult.value.camera.depthOfField !== undefined ||
2295
+ frameState.depthOfFieldAccepted !== undefined);
2175
2296
  if (volumeTopologyCandidate) {
2176
2297
  frameState.volumetricFogPreviousGraph = previous;
2177
2298
  frameState.volumetricFogPreviousGraphKey = frameState.compiledFrameGraphTopologyKey;
2178
2299
  frameState.volumetricFogCandidateGraph = compiled.value;
2300
+ if (depthOfFieldReplacement) {
2301
+ frameState.depthOfFieldPreviousGraph = previous;
2302
+ frameState.depthOfFieldPreviousGraphKey = frameState.compiledFrameGraphTopologyKey;
2303
+ frameState.depthOfFieldPreviousPerFrameGraph = frameState.perFrameGraph;
2304
+ frameState.depthOfFieldPreviousGraphGeneration = frameState.graphGeneration;
2305
+ frameState.depthOfFieldPreviousStandardLightingGraphSignature =
2306
+ frameState.standardLightingGraphSignature;
2307
+ frameState.depthOfFieldCandidateGraph = compiled.value;
2308
+ }
2309
+ } else if (depthOfFieldReplacement) {
2310
+ frameState.depthOfFieldPreviousGraph = previous;
2311
+ frameState.depthOfFieldPreviousGraphKey = frameState.compiledFrameGraphTopologyKey;
2312
+ frameState.depthOfFieldPreviousPerFrameGraph = frameState.perFrameGraph;
2313
+ frameState.depthOfFieldPreviousGraphGeneration = frameState.graphGeneration;
2314
+ frameState.depthOfFieldPreviousStandardLightingGraphSignature =
2315
+ frameState.standardLightingGraphSignature;
2316
+ frameState.depthOfFieldCandidateGraph = compiled.value;
2179
2317
  }
2180
2318
  frameState.compiledFrameGraph = compiled.value;
2181
2319
  frameState.compiledFrameGraphTopologyKey = key;
@@ -2209,10 +2347,99 @@ export function ensureCompiledFrameGraph(
2209
2347
  } else if (featureGraphCandidate !== undefined) {
2210
2348
  recordRenderFeatureCandidateEvent(internals, featureGraphCandidate, 'abandoned');
2211
2349
  }
2212
- if (!volumeTopologyCandidate && previous !== null) retire(frameState, previous);
2350
+ if (!volumeTopologyCandidate && !depthOfFieldReplacement && previous !== null) {
2351
+ retire(frameState, previous);
2352
+ }
2213
2353
  return compiled.value;
2214
2354
  }
2215
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
+
2216
2443
  export function executeCompiledFrameGraph(
2217
2444
  internals: RenderSystemInternals,
2218
2445
  frameState: RenderFrameState,
@@ -2245,9 +2472,11 @@ export function executeCompiledFrameGraph(
2245
2472
  frameState.pendingAutoExposureState = undefined;
2246
2473
  frameState.pendingStandardLutState = undefined;
2247
2474
  settleVolumetricFogGraphCandidate(frameState, false);
2475
+ settleDepthOfFieldGraphCandidate(frameState, false);
2248
2476
  rejectTemporalFrame();
2249
2477
  return false;
2250
2478
  }
2479
+ const motionBlurExecution = deriveMotionBlurExecutionReceipt(internals, frameState);
2251
2480
  const legacyTimingInstrumentation =
2252
2481
  timingCapture === undefined ? undefined : createTimingInstrumentation(timingCapture);
2253
2482
  const timingHost = internals as RenderSystemInternals & {
@@ -2308,6 +2537,8 @@ export function executeCompiledFrameGraph(
2308
2537
  },
2309
2538
  }),
2310
2539
  );
2540
+ settleVolumetricFogGraphCandidate(frameState, false);
2541
+ settleDepthOfFieldGraphCandidate(frameState, false);
2311
2542
  return false;
2312
2543
  }
2313
2544
  const capturedGeneration = deviceScope?.generation;
@@ -2868,6 +3099,11 @@ export function executeCompiledFrameGraph(
2868
3099
  commit: () => {
2869
3100
  resolvedFrameHooks?.onSubmitted?.();
2870
3101
  legacySubmitCommit?.();
3102
+ if (motionBlurExecution === undefined) {
3103
+ delete frameState.motionBlurExecution;
3104
+ } else {
3105
+ frameState.motionBlurExecution = motionBlurExecution;
3106
+ }
2871
3107
  if (frameState.temporalFrameInput !== undefined) {
2872
3108
  const temporal = frameState.temporalFrameTransaction.commit({ accepted: true });
2873
3109
  if (temporal.ok) {
@@ -3000,6 +3236,8 @@ export function executeCompiledFrameGraph(
3000
3236
  frameState.pendingAtmospherePublish?.();
3001
3237
  frameState.pendingAtmospherePublish = undefined;
3002
3238
  onSubmitted?.(internals.device.queue.onSubmittedWorkDone());
3239
+ settleVolumetricFogGraphCandidate(frameState, true);
3240
+ settleDepthOfFieldGraphCandidate(frameState, true);
3003
3241
  },
3004
3242
  abort: (failure) => {
3005
3243
  frameState.pendingAtmospherePublish = undefined;
@@ -3038,9 +3276,15 @@ export function executeCompiledFrameGraph(
3038
3276
  frameState.environmentLifecycle?.discard(frameState.environmentGeneration);
3039
3277
  frameState.environmentGeneration = undefined;
3040
3278
  }
3279
+ settleVolumetricFogGraphCandidate(frameState, false);
3280
+ settleDepthOfFieldGraphCandidate(frameState, false);
3041
3281
  },
3042
3282
  });
3283
+ // The callbacks above settle the normal transaction path. Keep this guard
3284
+ // for synthetic callers and any future early transaction result that does
3285
+ // not enter a callback.
3043
3286
  settleVolumetricFogGraphCandidate(frameState, transaction.ok);
3287
+ settleDepthOfFieldGraphCandidate(frameState, transaction.ok);
3044
3288
  if (!transaction.ok) rejectTemporalFrame();
3045
3289
  return transaction.ok;
3046
3290
  }
@@ -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 {
@@ -232,6 +234,12 @@ export function createRenderSystemRecovery(
232
234
  directionalShadowCacheRecorded: false,
233
235
  compiledFrameGraph: null,
234
236
  compiledFrameGraphTopologyKey: null,
237
+ depthOfFieldPreviousGraph: null,
238
+ depthOfFieldPreviousGraphKey: null,
239
+ depthOfFieldPreviousPerFrameGraph: undefined,
240
+ depthOfFieldPreviousGraphGeneration: 0,
241
+ depthOfFieldPreviousStandardLightingGraphSignature: '',
242
+ depthOfFieldCandidateGraph: null,
235
243
  volumetricFogPreviousGraph: null,
236
244
  volumetricFogPreviousGraphKey: null,
237
245
  volumetricFogCandidateGraph: null,
@@ -526,6 +534,10 @@ export function createRenderSystemRecovery(
526
534
  worlds: seed.worlds,
527
535
  owner: seed.resourceOwner,
528
536
  frameNumber: candidateFrameState.frameNumber,
537
+ frameSize: {
538
+ width: runtime.internals.canvas.width,
539
+ height: runtime.internals.canvas.height,
540
+ },
529
541
  visibilitySnapshots: seed.frame.featureVisibilitySnapshots,
530
542
  hiddenEntityReports: seed.frame.hiddenEntityReports,
531
543
  targets: featureTargets,
@@ -788,29 +800,35 @@ export function createRenderSystemRecovery(
788
800
  throw new Error('recovery TAA pipeline is unavailable');
789
801
  }
790
802
  }
791
- 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
+ ) {
792
812
  warmPostProcessPipeline('forgeax.motion-blur', 'rgba16float');
793
813
  }
794
814
  for (const id of candidatePostProcessIds) {
795
815
  warmPostProcessPipeline(id, runtime.pipelineState.format as GPUTextureFormat);
796
816
  }
797
- if (camera.bloom === 'on' && camera.tonemap !== 'none') {
817
+ if (standardBloomAdmitted(camera)) {
798
818
  runtime.pipelineState.perPassResources.ensureBloomResources?.();
799
819
  const bloom = runtime.pipelineState.perPassResources.getBloomResources?.();
800
820
  if (
801
821
  bloom === null ||
802
822
  bloom === undefined ||
803
- bloom.bloomBrightPipeline === null ||
804
- bloom.bloomBlurHPipeline === null ||
805
- bloom.bloomBlurVPipeline === null ||
823
+ bloom.bloomDownsamplePipeline === null ||
824
+ bloom.bloomUpsamplePipeline === null ||
806
825
  bloom.bloomCompositePipeline === null ||
807
- bloom.bloomBrightBindGroupLayout === null ||
808
- bloom.bloomBlurBindGroupLayout === null ||
826
+ bloom.bloomDownsampleBindGroupLayout === null ||
827
+ bloom.bloomUpsampleBindGroupLayout === null ||
809
828
  bloom.bloomCompositeBindGroupLayout === null ||
810
829
  bloom.bloomSampler === null ||
811
- bloom.bloomBrightParamsBuffer === null ||
812
- bloom.bloomBlurHParamsBuffer === null ||
813
- bloom.bloomBlurVParamsBuffer === null ||
830
+ bloom.bloomDownsampleParamsBuffer === null ||
831
+ bloom.bloomUpsampleParamsBuffer === null ||
814
832
  bloom.bloomCompositeParamsBuffer === null
815
833
  ) {
816
834
  throw new Error('recovery Bloom resources are incomplete');
@@ -31,10 +31,15 @@ import type { EnvironmentInspection } from './environment/inspection.js';
31
31
  import type { GpuDrivenPreparationError } from './errors/gpu-driven.js';
32
32
  import type { RecoveryOutcome, RecoveryPhase } from './errors/recover.js';
33
33
  import type { RenderError } from './errors/render.js';
34
+ import type {
35
+ DepthOfFieldParams,
36
+ DepthOfFieldRequestFailure,
37
+ } from './features/depth-of-field/depth-of-field-params.js';
34
38
  import type { RenderFeature, RenderFeatureDiagnostics } from './features/types.js';
35
39
  import type { PostProcessShaderEntry } from './fullscreen-post-process-pass.js';
36
40
  import type {
37
41
  BloomInspection,
42
+ DepthOfFieldInspection,
38
43
  DirectionalShadowInspection,
39
44
  LightInspection,
40
45
  LodOcclusionInspection,
@@ -165,17 +170,24 @@ export interface CameraSnapshot {
165
170
  readonly bloom: BloomEnabled;
166
171
  readonly bloomThreshold: number;
167
172
  readonly bloomIntensity: number;
168
- readonly bloomBlurRadius: number;
173
+ readonly bloomSoftKnee: number;
174
+ readonly bloomScatter: number;
169
175
  readonly clearColor: readonly [number, number, number, number];
170
176
  /** Optional active-camera SSR authoring fact, copied from the ECS schema. */
171
177
  readonly screenSpaceReflection?: ScreenSpaceReflectionData;
172
178
  /** Renderer-owned temporal projection used by Standard's TAA producer. */
173
179
  readonly temporal?: TemporalView;
180
+ /** Active-camera depth-of-field parameters, present only when demanded. */
181
+ readonly depthOfField?: DepthOfFieldParams;
182
+ /** Structured DoF validation facts; invalid requests remain inspectable. */
183
+ readonly depthOfFieldError?: DepthOfFieldRequestFailure;
174
184
  /** Active-camera motion blur parameters, present only when demanded. */
175
185
  readonly motionBlur?: {
176
186
  readonly shutterAngle: number;
177
187
  readonly maxRadiusPixels: number;
178
188
  readonly sampleCount: number;
189
+ /** Target presentation rate; zero follows the accepted frame interval. */
190
+ readonly targetFps?: number;
179
191
  };
180
192
  }
181
193
 
@@ -557,6 +569,19 @@ export interface RenderFrameInput {
557
569
  readonly leases: readonly RenderWorldLease[];
558
570
  readonly camera: FrameCamera;
559
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;
560
585
  /** Fixed-step publication consumed by dynamic geometry candidates. */
561
586
  readonly fixedStep?: number;
562
587
  /** Optional profiler correlation token owned by App and consumed by Render. */
@@ -776,6 +801,8 @@ export interface RenderInspection {
776
801
  readonly temporalTarget?: TemporalTargetInspection;
777
802
  /** Renderer-owned Motion Blur facts; absent when no component is active. */
778
803
  readonly motionBlur?: MotionBlurInspection;
804
+ /** Renderer-owned Depth of Field facts; absent when no component is active. */
805
+ readonly depthOfField?: DepthOfFieldInspection;
779
806
  /** Bounded renderer-owned LOD/occlusion facts; absent before a candidate frame. */
780
807
  readonly lodOcclusion?: LodOcclusionInspection;
781
808
  }
@@ -800,6 +827,10 @@ export type DrawOwnerOptions = {
800
827
  readonly resourceOwner: number;
801
828
  /** Optional FrameCamera entity selection; absent preserves ActiveCamera semantics. */
802
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;
803
834
  readonly profileFrame?: ProfileFrameToken;
804
835
  };
805
836
 
@@ -898,9 +929,8 @@ export const RENDER_GRAPH_EXECUTION_PHASE_CATALOG = [
898
929
  'record/graph-execute/skybox',
899
930
  'record/graph-execute/main',
900
931
  'record/graph-execute/fxaa',
901
- 'record/graph-execute/bloom-bright',
902
- 'record/graph-execute/bloom-blur-h',
903
- 'record/graph-execute/bloom-blur-v',
932
+ 'record/graph-execute/bloom-downsample',
933
+ 'record/graph-execute/bloom-upsample',
904
934
  'record/graph-execute/bloom-composite',
905
935
  'record/graph-execute/ssr-hiz',
906
936
  'record/graph-execute/ssr-trace',
@@ -577,6 +577,7 @@ function dispatchFullscreenPass(
577
577
  graphDepthView?: TextureView,
578
578
  paramsOverride?: Uint8Array,
579
579
  fragmentEntryPoint?: string,
580
+ msaaActive = false,
580
581
  ): void {
581
582
  if (shader === 'fxaa') {
582
583
  if (resolveCtx === undefined) {
@@ -643,7 +644,7 @@ function dispatchFullscreenPass(
643
644
  const built = buildFullscreenPostProcessPass(
644
645
  { device: ctx.runtime.device, errorRegistry: ctx.runtime.errorRegistry },
645
646
  entry,
646
- ctx.msaaActive,
647
+ msaaActive,
647
648
  );
648
649
  if (built === null) return;
649
650
 
@@ -842,6 +843,8 @@ export function encodeFullscreenPass(
842
843
  readonly depthView?: TextureView | undefined;
843
844
  readonly paramsOverride?: Uint8Array | undefined;
844
845
  readonly fragmentEntryPoint?: string | undefined;
846
+ /** The depth target's actual sample count, when this typed pass reads depth. */
847
+ readonly msaaActive?: boolean | undefined;
845
848
  },
846
849
  ): void {
847
850
  dispatchFullscreenPass(
@@ -858,5 +861,9 @@ export function encodeFullscreenPass(
858
861
  input.depthView,
859
862
  input.paramsOverride,
860
863
  input.fragmentEntryPoint,
864
+ // Legacy callers rely on the frame context's MSAA state. Typed callers
865
+ // pass the actual depth target sample count explicitly so a single-sample
866
+ // view can never select the multisampled shader by camera policy alone.
867
+ input.msaaActive ?? ctx.msaaActive,
861
868
  );
862
869
  }