@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
@@ -1,14 +1,14 @@
1
- import { inspectVolumetricFog, resolveSsrAdmissionGeneration, inspectLodOcclusion, zeroSsrAdmissionWork, projectSsrSpatialInspection, createDynamicGeometryLifecycle, inspectReflectionFallback, admitReflectionProbe, DEFAULT_REFLECTION_PROBE_LIMITS, buildReflectionProbeTable, advanceProbeFilter, commitProbeFilterStep, resolveReflectionFallbackSource, deriveReflectionFallbackProjection, reflectionFallbackCoverage, reflectionFallbackSourceKey, reflectionFallbackProjectionSignature, createProbeFilterState, ReflectionProbeProjection, admitPointsLines, projectSsrDependencies, admitSsrSpatial, inspectPointShadow, DynamicGeometryError, sameCandidateCredential } from './chunk-LULCA3OF.mjs';
2
- import { createExtendedLightingState, promoteExtendedLightingCandidate, glyphTextLayoutSystem, projectExtendedLightingInspection, OcclusionRenderRuntime, extractFrames, viewKey, primitiveKey, viewKeyId, primitiveKeyId, ProbeBlendSceneProjection, SKYLIGHT_RECOVERY_FALLBACK, InstanceBoundsCache, fingerprintNumericArray, buildShadowFrusta, createSsrHistoryOwner, createSsrSpatialResources, createCookieProjectionMatrixData } from './chunk-4NNZQCL3.mjs';
1
+ import { inspectVolumetricFog, resolveSsrAdmissionGeneration, inspectLodOcclusion, zeroSsrAdmissionWork, projectSsrSpatialInspection, createDynamicGeometryLifecycle, inspectReflectionFallback, admitReflectionProbe, DEFAULT_REFLECTION_PROBE_LIMITS, buildReflectionProbeTable, advanceProbeFilter, commitProbeFilterStep, resolveReflectionFallbackSource, deriveReflectionFallbackProjection, reflectionFallbackCoverage, reflectionFallbackSourceKey, reflectionFallbackProjectionSignature, createProbeFilterState, ReflectionProbeProjection, admitPointsLines, projectSsrDependencies, admitSsrSpatial, inspectPointShadow, DynamicGeometryError, sameCandidateCredential } from './chunk-NNQL7CB3.mjs';
2
+ import { createExtendedLightingState, promoteExtendedLightingCandidate, glyphTextLayoutSystem, emptyBloomInspection, projectExtendedLightingInspection, OcclusionRenderRuntime, extractFrames, viewKey, primitiveKey, viewKeyId, primitiveKeyId, ProbeBlendSceneProjection, InstanceBoundsCache, fingerprintNumericArray, buildShadowFrusta, createSsrHistoryOwner, createSsrSpatialResources, createCookieProjectionMatrixData } from './chunk-OO3DRYK7.mjs';
3
3
  import './chunk-ILQ5MEZF.mjs';
4
- import { DEFAULT_DEPTH_OF_FIELD_PARAMS, MeshFilter, MeshRenderer, Instances, Layer, Visibility, Points, Lines, SortKey, projectMeshMaterialBindingObservation, resolveDepthOfFieldFrameParams, buildCubeCameraFaceViews, hasVolumetricFogCapability, packDepthOfFieldParams, Camera, CAMERA_PROJECTION_ORTHOGRAPHIC, CUBE_CAMERA_FACE_ORDER } from './chunk-GWCEMDVR.mjs';
4
+ import { DEFAULT_DEPTH_OF_FIELD_PARAMS, MeshFilter, MeshRenderer, Instances, Layer, Visibility, Points, Lines, SortKey, projectMeshMaterialBindingObservation, resolveDepthOfFieldFrameParams, buildCubeCameraFaceViews, SKYLIGHT_RECOVERY_FALLBACK, hasVolumetricFogCapability, packDepthOfFieldParams, Camera, CAMERA_PROJECTION_ORTHOGRAPHIC, CUBE_CAMERA_FACE_ORDER } from './chunk-PLAXOOX5.mjs';
5
5
  import { SpriteInstances, SpriteRegionOverride, TileLayer, decodeSortScope, Tilemap } from './chunk-PA5DRJNI.mjs';
6
- import { GPU_SCENE_LAYOUTS, deviceOptionsForAdapter, PipelineSpecError, assertStorageBufferCap, STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES, GpuBuffer, GpuTexture, deriveRenderDataTexture, deriveRenderDataMesh, deriveRenderDataCubemap, createIblPipelines, CUBEMAP_FACE_VERTICES, runIblPrecompute, createClusterBinScratch, resetHdrpBuffers, retireTemporalGpuState, PostProcessError, entryHasDepthRead, DEPTH_MIN_PARAMS_BYTE_SIZE, postProcessShaderModuleLabel, resolveSsaoParameters, buildPerFrameBindGroups, resolveMaterialSkylight, postProcessShaderEntrySignature, selectSwapChainFormat, setIblComposedShaders, POINTS_LINES_MATERIAL_SHADER_ID, selectStandardPbrTransmissionPrewarmVariants, selectHdrpPbrPrewarmVariants, prewarmMaterialShaderVariants, selectProbePrewarmVariants, createHdrpClusterMembershipBindGroupLayoutDescriptor, createSkylightFallback, buildSpecConstTable, buildLinearLdrMaterialSpecTable, getOrBuildPipeline, cacheKeyOf, configureSurface, CAPTURE_VIEW_PROJS, PREFILTER_MIP_LEVELS, REFLECTION_PROBE_VIEW_SLOT_BASE, VIEW_UNIFORM_SLOT_STRIDE, getOrCreateIblCache, decideVisibility, BatchTopology, postProcessShaderPipelineLabel, createPointsLinesLaneAdapter, computeViewMatrix, computeProjectionMatrix, prepareFrameLighting, warnMultiSkylight, warnMultiSkybox, selectLazyEquirectHandle, driveLazyEquirectProjection, warnZeroLightStandard, writeShadowParamsBuffer, deriveRenderExtent, writeSpotModifierTextures, writeHdrpClusterAndSsaoBuffers, resolveSkyboxActive, computeSplitLdrSprite, writeViewUbo, writePointsLinesViewUbo, writeShadowCasterUniforms, uploadMeshSsboBatch, passKindPolicyTable, colorFormatsForPassKind, GPU_DRIVEN_VIEW_WGSL, selectGpuDrivenSceneIndexVariant, selectSkinPrewarmVariants, transmissionBackdropAvailable, VIEW_UNIFORM_BUFFER_SIZE, POINTS_LINES_VIEW_BUFFER_SIZE, SHADOW_CASTER_BUFFER_SIZE, FALLBACK_BYTES_PER_ROW, getIblProbeBackgroundSource, OcclusionConfidenceScheduler, shouldIssueRetest, selectGpuLodLane, ShadowViewStatePool, GpuDrivenView, MATERIAL_PER_ENTITY_STRIDE, shadowViewIdentityKey, createPointsLinesLaneContract, foldDispatchBuckets, CUBE_CAPTURE_VIEW_SLOT_BASE, variantSetFromDefines, resolveReflectionProbeBinding, cleanPerEntityCache, ensureMeshSsboCapacity, buildFoldDispatchPlan, evaluateFoldBucketUniformCap, incrementFoldedDrawsMetric, writePbrMaterialUboPayload, applyParamSnapshotToUbo, applyMaterialTextureUvScales, residentTextureView, detectNineSliceScaleTooSmall, PREFILTER_SIZE, gpuSceneFieldOffset, projectedHeightForCandidate, geometryRenderStateForTopology, packInstanceStorageBuffer, MESH_SSBO_BYTES, buildFullscreenPostProcessPass, getTemporalGpuState, getTemporalParamsBuffer, getTemporalBindGroupResources, TEMPORAL_HISTORY_FORMATS, resolveSurfaceFormatPair, resolveSurfaceProfile, createRenderPipelineTarget, encodeMainPass, STANDARD_PBR_UBO_SIZE, abortTemporalGpuSubmit, commitTemporalGpuSubmit, retireTemporalGpuStateAfterFence, hasPendingTemporalGpuSubmit, importRenderPipelineSurface, renderExtentSize, DEFERRED_COLOR_FORMATS, aggregateTemporalDemand, standardTemporalLaneAdmission, addTypedScenePass, addTypedSsaoPasses, addTypedSkyboxPass, createStandardSceneDataTarget, addStandardSceneDataPass, VIEW_UNIFORM_BYTES, addTypedFrameObservationPass, addReflectionFallbackObservationPass, selectLod, temporalReadIndex, temporalWriteIndex, prepareMaterialSkylight, BYTES_PER_DIRECT_LIGHT_SLOT, shadowViewRasterAccesses, encodeDirectionalShadowPass, encodePointShadowPass, encodeSpotShadowPass, getOrCreateFromChain, resolveOutputDither, addTypedTemporalResolvePass, addTypedBloomPasses, addTypedOutputTransformPass, addTypedFullscreenPass, addTypedCompositePostEffects, standardTopologyVariantSet, geometryRenderStateForPass, standardStorageVariantSet, getOrCreateHdrpBuffers, variantSetFromVertexLayoutProjection, createStandardSurfaceLightingBindGroup, isTemporalFullscreenBinding, createFullscreenBindGroup, applyParamSchemaDefaultsToUbo, defaultViewForUserRegionField, assembleMaterialWithSkylightEntries, TYPED_BLOOM_PASS_NAMES } from './chunk-JRIPCMYC.mjs';
6
+ import { GPU_SCENE_LAYOUTS, deviceOptionsForAdapter, PipelineSpecError, assertStorageBufferCap, STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES, GpuBuffer, GpuTexture, deriveRenderDataTexture, deriveRenderDataMesh, deriveRenderDataCubemap, createIblPipelines, CUBEMAP_FACE_VERTICES, runIblPrecompute, createClusterBinScratch, resetHdrpBuffers, retireTemporalGpuState, PostProcessError, entryHasDepthRead, DEPTH_MIN_PARAMS_BYTE_SIZE, postProcessShaderModuleLabel, resolveSsaoParameters, buildPerFrameBindGroups, resolveMaterialSkylight, postProcessShaderEntrySignature, standardBloomAdmitted, selectSwapChainFormat, setIblComposedShaders, POINTS_LINES_MATERIAL_SHADER_ID, selectStandardPbrTransmissionPrewarmVariants, selectHdrpPbrPrewarmVariants, prewarmMaterialShaderVariants, selectProbePrewarmVariants, createHdrpClusterMembershipBindGroupLayoutDescriptor, createSkylightFallback, buildSpecConstTable, buildLinearLdrMaterialSpecTable, getOrBuildPipeline, cacheKeyOf, HDR_COLOR_ATTACHMENT_FORMAT, configureSurface, CAPTURE_VIEW_PROJS, PREFILTER_MIP_LEVELS, REFLECTION_PROBE_VIEW_SLOT_BASE, VIEW_UNIFORM_SLOT_STRIDE, getOrCreateIblCache, decideVisibility, BatchTopology, postProcessShaderPipelineLabel, createPointsLinesLaneAdapter, computeViewMatrix, computeProjectionMatrix, prepareFrameLighting, warnMultiSkylight, warnMultiSkybox, selectLazyEquirectHandle, driveLazyEquirectProjection, warnZeroLightStandard, writeShadowParamsBuffer, deriveRenderExtent, writeSpotModifierTextures, writeHdrpClusterAndSsaoBuffers, resolveSkyboxActive, computeSplitLdrSprite, writeViewUbo, writePointsLinesViewUbo, writeShadowCasterUniforms, uploadMeshSsboBatch, passKindPolicyTable, colorFormatsForPassKind, GPU_DRIVEN_VIEW_WGSL, selectGpuDrivenSceneIndexVariant, selectSkinPrewarmVariants, transmissionBackdropAvailable, VIEW_UBO_BYTES, POINTS_LINES_VIEW_BUFFER_SIZE, SHADOW_CASTER_BUFFER_SIZE, FALLBACK_BYTES_PER_ROW, MIPMAP_PREWARM_FORMATS, getIblProbeBackgroundSource, OcclusionConfidenceScheduler, shouldIssueRetest, selectGpuLodLane, ShadowViewStatePool, GpuDrivenView, MATERIAL_PER_ENTITY_STRIDE, shadowViewIdentityKey, createPointsLinesLaneContract, foldDispatchBuckets, CUBE_CAPTURE_VIEW_SLOT_BASE, variantSetFromDefines, resolveReflectionProbeBinding, cleanPerEntityCache, ensureMeshSsboCapacity, buildFoldDispatchPlan, evaluateFoldBucketUniformCap, incrementFoldedDrawsMetric, writePbrMaterialUboPayload, applyParamSnapshotToUbo, applyMaterialTextureUvScales, residentTextureView, detectNineSliceScaleTooSmall, DEPTH_TEXTURE_FORMAT, PREFILTER_SIZE, gpuSceneFieldOffset, projectedHeightForCandidate, geometryRenderStateForTopology, packInstanceStorageBuffer, MESH_SSBO_BYTES, buildFullscreenPostProcessPass, getTemporalGpuState, getTemporalParamsBuffer, getTemporalBindGroupResources, TEMPORAL_HISTORY_FORMATS, resolveSurfaceFormatPair, resolveSurfaceProfile, createRenderPipelineTarget, encodeMainPass, STANDARD_PBR_UBO_SIZE, abortTemporalGpuSubmit, commitTemporalGpuSubmit, retireTemporalGpuStateAfterFence, hasPendingTemporalGpuSubmit, importRenderPipelineSurface, renderExtentSize, DEFERRED_COLOR_FORMATS, aggregateTemporalDemand, standardTemporalLaneAdmission, addTypedScenePass, addTypedSsaoPasses, addTypedSkyboxPass, createStandardSceneDataTarget, addStandardSceneDataPass, VIEW_UNIFORM_BYTES, addTypedFrameObservationPass, addReflectionFallbackObservationPass, selectLod, temporalReadIndex, temporalWriteIndex, prepareMaterialSkylight, BYTES_PER_DIRECT_LIGHT_SLOT, shadowViewRasterAccesses, encodeDirectionalShadowPass, encodePointShadowPass, encodeSpotShadowPass, getOrCreateFromChain, resolveOutputDither, addTypedFullscreenPass, addTypedTemporalResolvePass, addTypedBloomPasses, addTypedOutputTransformPass, addTypedCompositePostEffects, standardTopologyVariantSet, geometryRenderStateForPass, standardStorageVariantSet, getOrCreateHdrpBuffers, variantSetFromVertexLayoutProjection, createStandardSurfaceLightingBindGroup, BLOOM_DOWNSAMPLE_PARAMS_BYTES, BLOOM_UPSAMPLE_PARAMS_BYTES, BLOOM_COMPOSITE_PARAMS_BYTES, isTemporalFullscreenBinding, createFullscreenBindGroup, applyParamSchemaDefaultsToUbo, defaultViewForUserRegionField, assembleMaterialWithSkylightEntries, TYPED_BLOOM_PASS_NAMES } from './chunk-VOZWDM3L.mjs';
7
7
  import { getTransparentSortConfig, TRANSPARENT_SORT_MODE_DISTANCE, TRANSPARENT_SORT_MODE_LAYER_Z, TRANSPARENT_SORT_MODE_LAYER_Y, TRANSPARENT_SORT_MODE_LAYER_YZ } from './chunk-RXZ3RGOU.mjs';
8
- import { DEFAULT_STANDARD_PROFILE, DEFAULT_CLUSTER_GRID, STANDARD_PIPELINE_ID, STANDARD_LIGHT_COUNTS, STANDARD_POST_STAGE_NAMES, createSceneDataCatalog, resolveVolumetricFogProfile, isSceneDataTarget } from './chunk-S665NJNF.mjs';
8
+ import { DEFAULT_STANDARD_PROFILE, DEFAULT_CLUSTER_GRID, STANDARD_PIPELINE_ID, STANDARD_LIGHT_COUNTS, STANDARD_POST_STAGE_NAMES, createSceneDataCatalog, isSceneDataTarget, resolveVolumetricFogProfile, SCENE_DATA_TEMPORAL_V1_SCHEMA } from './chunk-LEAXOMYJ.mjs';
9
9
  import { SPRITE_PREMULTIPLIED_ALPHA_BLEND } from './chunk-BLXGONR2.mjs';
10
- import { DeviceScope, SKIN_MATERIAL_SHADER_ID, EXTENDED_LIGHTING_TOPOLOGY, getTextureIdentity, GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_MAP_READ, resolveRenderTargetMaterialSource, createRenderTargetMaterialSource, deriveExtendedLightingCapability, GPU_BUFFER_USAGE_VERTEX, GPU_BUFFER_USAGE_INDEX, IES_SLICE_WIDTH, IES_SLICE_HEIGHT, COOKIE_SLICE_SIZE, COOKIE_MATRIX_BYTES, GPU_BUFFER_USAGE_UNIFORM, worldEntityKey, createHdrpBindGroupLayoutDescriptor, createHdrpSkinBindGroupLayoutDescriptor, GPU_BUFFER_USAGE_QUERY_RESOLVE, GPU_BUFFER_USAGE_COPY_SRC, gpuDrivenShadowDrawKey, makeZeroCameraFallbackSnapshot, buildPbrPipelineLayouts, buildPbrSkinLayouts, buildGpuDrivenPbrInstancesBindGroupLayout, buildGpuDrivenPbrPipelineLayout, buildGpuDrivenPbrSkinPipelineLayout, GPU_BUFFER_USAGE_STORAGE, resolvePipelineGroup2Contract, GPU_SHADER_STAGE_FRAGMENT, gpuDrivenDrawKey, gpuDrivenSourceDrawItemIndex, instanceCollectionCacheKey, validateGraphTargetCaptureReadback, createTemporalCoverage, isCanonicalStandardPbrMaterialShader, isStandardPbrMaterialShader, buildBindGroupLayoutDescriptor, gpuDrivenMaterialArtifactKey, buildPbrMaterialUserRegionEntries, extendedLightingSampledTextureCapacityAvailable, GPU_SHADER_STAGE_VERTEX, SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET, SHADOW_CASTER_SHADER_ID, shadowCasterVariantSet, GPU_SHADER_STAGE_COMPUTE, LifecycleTransaction } from './chunk-DRR7D3KX.mjs';
11
- import { RendererContractFailureError, RendererOperationError, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, FrameReceiptStaleError, RenderFeatureStageFailedError, RenderFeatureCapabilityMissingError, RenderTargetStateInvalidError, RenderTargetOperationFailedError, RenderTargetCapabilityMissingError, RenderTargetDescriptorInvalidError, GPU_TEXTURE_USAGE_TEXTURE_BINDING, GPU_TEXTURE_USAGE_COPY_DST, RENDER_PHASE_CATALOG, InstanceCollectionStore, createAutoExposureInspection, RenderFeatureRegistrationConflictError, GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING, GPU_TEXTURE_USAGE_COPY_SRC, createVisibilityBudget, EnvironmentGenerationFailedError, PointsLinesMaterialUnsupportedError, StandardProfileInvalidError, TransmissionCapabilityMissingError, SHADOW_ATLAS_DEFAULT_LAYERS, GPU_TEXTURE_USAGE_RENDER_ATTACHMENT, GpuDrivenPreparationError, TemporalFrameSubmitError, ObservationUnavailableError, RenderFeaturePreparationFailedError, AutoExposureInvalidParameterError, PointsLinesPrepareFailedError, SHADOW_ATLAS_DEFAULT_FACE_SIZE, SceneDataUnavailableError, SkinPaletteOverflowError, GPU_TEXTURE_USAGE_STORAGE_BINDING, RenderFeatureDrawRecordingFailedError, PointShadowAtlasUninitializedError, RenderFeaturePreparedStateMismatchError, FXAA_POST_PROCESS_ID } from './chunk-6TFBXLRJ.mjs';
10
+ import { DeviceScope, SKIN_MATERIAL_SHADER_ID, EXTENDED_LIGHTING_TOPOLOGY, getTextureIdentity, GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_MAP_READ, resolveRenderTargetMaterialSource, createRenderTargetMaterialSource, deriveExtendedLightingCapability, GPU_BUFFER_USAGE_VERTEX, GPU_BUFFER_USAGE_INDEX, IES_SLICE_WIDTH, IES_SLICE_HEIGHT, COOKIE_SLICE_SIZE, COOKIE_MATRIX_BYTES, GPU_BUFFER_USAGE_UNIFORM, worldEntityKey, createHdrpBindGroupLayoutDescriptor, createHdrpSkinBindGroupLayoutDescriptor, GPU_BUFFER_USAGE_QUERY_RESOLVE, GPU_BUFFER_USAGE_COPY_SRC, gpuDrivenShadowDrawKey, makeZeroCameraFallbackSnapshot, buildPbrPipelineLayouts, buildPbrSkinLayouts, buildGpuDrivenPbrInstancesBindGroupLayout, buildGpuDrivenPbrPipelineLayout, buildGpuDrivenPbrSkinPipelineLayout, GPU_BUFFER_USAGE_STORAGE, resolvePipelineGroup2Contract, GPU_SHADER_STAGE_FRAGMENT, gpuDrivenDrawKey, gpuDrivenSourceDrawItemIndex, instanceCollectionCacheKey, validateGraphTargetCaptureReadback, createTemporalCoverage, isCanonicalStandardPbrMaterialShader, isStandardPbrMaterialShader, buildBindGroupLayoutDescriptor, gpuDrivenMaterialArtifactKey, hasRequiredVertexInputs, buildPbrMaterialUserRegionEntries, extendedLightingSampledTextureCapacityAvailable, GPU_SHADER_STAGE_VERTEX, SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET, SHADOW_CASTER_SHADER_ID, shadowCasterVariantSet, GPU_SHADER_STAGE_COMPUTE, LifecycleTransaction } from './chunk-3RTCJTH4.mjs';
11
+ import { RendererContractFailureError, RendererOperationError, MotionBlurValidationError, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, FrameReceiptStaleError, RenderFeatureStageFailedError, RenderFeatureCapabilityMissingError, RenderTargetStateInvalidError, RenderTargetOperationFailedError, RenderTargetCapabilityMissingError, RenderTargetDescriptorInvalidError, GPU_TEXTURE_USAGE_TEXTURE_BINDING, GPU_TEXTURE_USAGE_COPY_DST, RENDER_PHASE_CATALOG, InstanceCollectionStore, motionBlurTemporalDemand, createAutoExposureInspection, RenderFeatureRegistrationConflictError, GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING, GPU_TEXTURE_USAGE_COPY_SRC, createVisibilityBudget, EnvironmentGenerationFailedError, PointsLinesMaterialUnsupportedError, StandardProfileInvalidError, TransmissionCapabilityMissingError, motionBlurSampleDelta, isMotionBlurIntervalValid, MOTION_BLUR_PARAMS_BYTE_SIZE, effectiveMotionBlurSampleCount, motionBlurExposureScale, SHADOW_ATLAS_DEFAULT_LAYERS, DEFAULT_MOTION_BLUR_PARAMS, GPU_TEXTURE_USAGE_RENDER_ATTACHMENT, GpuDrivenPreparationError, TemporalFrameSubmitError, ObservationUnavailableError, RenderFeaturePreparationFailedError, AutoExposureInvalidParameterError, PointsLinesPrepareFailedError, validateMotionBlurParams, SHADOW_ATLAS_DEFAULT_FACE_SIZE, SceneDataUnavailableError, SkinPaletteOverflowError, GPU_TEXTURE_USAGE_STORAGE_BINDING, RenderFeatureDrawRecordingFailedError, PointShadowAtlasUninitializedError, RenderFeaturePreparedStateMismatchError, FXAA_POST_PROCESS_ID } from './chunk-NB2S7ZRX.mjs';
12
12
  import { ok, err, RhiError, validateDrawArgs } from '@forgeax/engine-rhi';
13
13
  import { AssetRegistry, DynamicTextureStore, adaptDynamicTextureDevice, getOrCreateMipmapPipeline, prepareMipmaps, blitMipmapsSync, HANDLE_CUBE, HANDLE_TRIANGLE, HANDLE_QUAD, HANDLE_SPHERE, HANDLE_NINESLICE_QUAD, BuiltinAssetRegistry, resolveAssetHandle, numMipLevels, encodeMipmapLevel, resolveTilesetRuntime, MeshSsboCeilingReachedError, MeshSsboCapacityExceededError } from '@forgeax/engine-assets-runtime';
14
14
  import { audioLoader } from '@forgeax/engine-audio-webaudio';
@@ -18,7 +18,7 @@ import { buildMeshAttributeMapForUvSets, deriveVertexBufferLayout, DEFAULT_VERTE
18
18
  import { GlobalTransform, ChildOf, MorphWeights, registerPropagateTransforms, collectSubtree, Name, Children, Transform } from '@forgeax/engine-scene';
19
19
  import { STANDARD_PIPELINE_PARAM_SCHEMA, createStandardPbrArtifactReceipt, ShaderCatalog, findVariantByKey, LTC_TABLES, standardPhysicalTextureFields, LTC_TABLE_HEIGHT, LTC_TABLE_WIDTH, DEFAULT_STANDARD_PBR_PARAM_SCHEMA, AUTO_EXPOSURE_METER_WGSL } from '@forgeax/engine-shader';
20
20
  import { halfFloat, frustum, mat4, vec3, box3, mat3 } from '@forgeax/engine-math';
21
- import { derive, err as err$1, ok as ok$1, handleSlot, toShared, deriveTextureLayout, ASSET_ERROR_HINTS, countExtraUvSets, RenderQueue, IMAGE_ERROR_HINTS, BUILTIN_BASE, unwrapHandle, materialValuesToLinearRuntime } from '@forgeax/engine-types';
21
+ import { derive, err as err$1, ok as ok$1, handleSlot, deriveTextureLayout, ASSET_ERROR_HINTS, countExtraUvSets, toShared, RenderQueue, IMAGE_ERROR_HINTS, BUILTIN_BASE, unwrapHandle, materialValuesToLinearRuntime } from '@forgeax/engine-types';
22
22
  import { createCurrentFrameObservationLease, RenderGraphBuilder, RenderGraphError } from '@forgeax/engine-render-graph';
23
23
  import { componentId } from '@forgeax/engine-ecs/internal';
24
24
  import { Skin } from '@forgeax/engine-skinning';
@@ -254,6 +254,7 @@ function exposeRenderer(renderer) {
254
254
  cause: structuredRendererCause(cause, "renderer-event")
255
255
  });
256
256
  const drawError = (cause) => {
257
+ if (cause instanceof MotionBlurValidationError) return cause;
257
258
  if (cause instanceof RendererOperationError) return cause;
258
259
  if (cause instanceof RendererContractFailureError) {
259
260
  return new RendererOperationError("device-operation-failed", {
@@ -849,6 +850,12 @@ var GpuResidencyCache = class {
849
850
  // equirect source always resolves to the same cubemap (idempotent, A2).
850
851
  cubemapIdempotentMap = /* @__PURE__ */ new Map();
851
852
  meshGpuHandles = /* @__PURE__ */ new Map();
853
+ /**
854
+ * Lower-detail POD identities packed into each resident mesh entry. The
855
+ * GPU entry only carries byte ranges; keeping the source identities beside
856
+ * it lets a later draw upgrade a root-only upload without hashing vertices.
857
+ */
858
+ meshLodAssets = /* @__PURE__ */ new WeakMap();
852
859
  meshLifetimes = /* @__PURE__ */ new WeakMap();
853
860
  retiredMeshes = /* @__PURE__ */ new Set();
854
861
  meshLifetime(entry) {
@@ -1063,7 +1070,7 @@ var GpuResidencyCache = class {
1063
1070
  return { freed, errors };
1064
1071
  }
1065
1072
  evictMesh(handle, worldId = 0) {
1066
- const id = this.worldKey(handleSlot(handle), worldId);
1073
+ const id = this.worldKey(Number(handle), worldId);
1067
1074
  const entry = this.meshGpuHandles.get(id);
1068
1075
  if (entry === void 0) return { freed: 0, errors: [] };
1069
1076
  if (this.meshLifetime(entry).owners > 0) return { freed: 0, errors: [] };
@@ -1271,11 +1278,11 @@ var GpuResidencyCache = class {
1271
1278
  * as the canonical "mesh asset has GPU residency" probe.
1272
1279
  */
1273
1280
  getMeshGpuHandles(handle, worldId = 0) {
1274
- return this.meshGpuHandles.get(this.worldKey(handleSlot(handle), worldId));
1281
+ return this.meshGpuHandles.get(this.worldKey(Number(handle), worldId));
1275
1282
  }
1276
1283
  /** Candidate leases retain the concrete allocation even after cache invalidation. */
1277
1284
  retainMeshResidency(handle, worldId = 0) {
1278
- const key = this.worldKey(handleSlot(handle), worldId);
1285
+ const key = this.worldKey(Number(handle), worldId);
1279
1286
  const entry = this.meshGpuHandles.get(key);
1280
1287
  return entry === void 0 ? void 0 : this.meshLifetime(entry).retain(() => {
1281
1288
  if (this.meshGpuHandles.get(key) === entry) this.invalidateMesh(handle, worldId);
@@ -1284,7 +1291,7 @@ var GpuResidencyCache = class {
1284
1291
  }
1285
1292
  /** In-place author changes immediately miss the cache; old submissions keep their allocation. */
1286
1293
  invalidateMesh(handle, worldId = 0) {
1287
- const key = this.worldKey(handleSlot(toShared(handle)), worldId);
1294
+ const key = this.worldKey(handle, worldId);
1288
1295
  const entry = this.meshGpuHandles.get(key);
1289
1296
  if (entry !== void 0) this.retireMeshEntry(key, entry);
1290
1297
  }
@@ -1292,9 +1299,20 @@ var GpuResidencyCache = class {
1292
1299
  const id = handleSlot(handle);
1293
1300
  switch (pod.kind) {
1294
1301
  case "mesh": {
1295
- const key = this.worldKey(id, worldId);
1302
+ const key = this.worldKey(Number(handle), worldId);
1296
1303
  const existing = this.meshGpuHandles.get(key);
1297
- if (existing !== void 0) return ok(existing);
1304
+ if (existing !== void 0) {
1305
+ if (lodMeshes.length === 0) return ok(existing);
1306
+ const previousLodAssets = this.meshLodAssets.get(existing);
1307
+ const sameLodChain = previousLodAssets !== void 0 && previousLodAssets.length === lodMeshes.length && previousLodAssets.every((asset, index) => asset === lodMeshes[index]);
1308
+ if (sameLodChain) return ok(existing);
1309
+ const projected2 = deriveRenderDataMesh(pod);
1310
+ if (!projected2.ok) return projected2;
1311
+ const upgraded = this.uploadMeshById(key, pod, projected2.value, lodMeshes);
1312
+ if (!upgraded.ok) return upgraded;
1313
+ this.retireMeshEntry(key, existing);
1314
+ return upgraded;
1315
+ }
1298
1316
  const projected = deriveRenderDataMesh(pod);
1299
1317
  if (!projected.ok) return projected;
1300
1318
  return this.uploadMeshById(key, pod, projected.value, lodMeshes);
@@ -1319,7 +1337,7 @@ var GpuResidencyCache = class {
1319
1337
  prepareResidentForRecovery(handle, pod, worldId = 0) {
1320
1338
  const id = handleSlot(handle);
1321
1339
  if (pod.kind === "mesh") {
1322
- const key = this.worldKey(id, worldId);
1340
+ const key = this.worldKey(Number(handle), worldId);
1323
1341
  if (this.meshGpuHandles.has(key)) return ok({});
1324
1342
  const projected2 = deriveRenderDataMesh(pod);
1325
1343
  if (!projected2.ok) return projected2;
@@ -1965,6 +1983,10 @@ var GpuResidencyCache = class {
1965
1983
  if (!vboResult.ok) return vboResult;
1966
1984
  const vbo = vboResult.value;
1967
1985
  let ibo = null;
1986
+ const cleanupUpload = () => {
1987
+ if (ibo !== null) device.destroyBuffer(ibo);
1988
+ device.destroyBuffer(vbo);
1989
+ };
1968
1990
  if (indices !== void 0) {
1969
1991
  const iboResult = device.createBuffer({
1970
1992
  label: `mesh-${id}-ibo`,
@@ -1972,15 +1994,22 @@ var GpuResidencyCache = class {
1972
1994
  usage: renderData.indexUsage,
1973
1995
  mappedAtCreation: false
1974
1996
  });
1975
- if (!iboResult.ok) return iboResult;
1997
+ if (!iboResult.ok) {
1998
+ cleanupUpload();
1999
+ return iboResult;
2000
+ }
1976
2001
  ibo = iboResult.value;
1977
2002
  }
1978
2003
  const vboWriteResult = device.queue.writeBuffer(vbo, 0, vertices);
1979
- if (!vboWriteResult.ok) return err(vboWriteResult.error);
2004
+ if (!vboWriteResult.ok) {
2005
+ cleanupUpload();
2006
+ return err(vboWriteResult.error);
2007
+ }
1980
2008
  if (indices !== void 0) {
1981
2009
  const indexSrc = new Uint8Array(indexBytes);
1982
2010
  indexSrc.set(new Uint8Array(indices.buffer, indices.byteOffset, indexBytesUnpadded));
1983
- if (ibo === null)
2011
+ if (ibo === null) {
2012
+ cleanupUpload();
1984
2013
  return err(
1985
2014
  new RhiError({
1986
2015
  code: "webgpu-runtime-error",
@@ -1988,8 +2017,12 @@ var GpuResidencyCache = class {
1988
2017
  hint: "the indexed mesh upload lost its RHI index-buffer owner"
1989
2018
  })
1990
2019
  );
2020
+ }
1991
2021
  const iboWriteResult = device.queue.writeBuffer(ibo, 0, indexSrc);
1992
- if (!iboWriteResult.ok) return err(iboWriteResult.error);
2022
+ if (!iboWriteResult.ok) {
2023
+ cleanupUpload();
2024
+ return err(iboWriteResult.error);
2025
+ }
1993
2026
  }
1994
2027
  const extraUvSets = countExtraUvSets(mesh.attributes);
1995
2028
  const entry = {
@@ -2008,6 +2041,7 @@ var GpuResidencyCache = class {
2008
2041
  ...lodRanges === void 0 ? {} : { lodRanges }
2009
2042
  };
2010
2043
  this.meshGpuHandles.set(id, entry);
2044
+ this.meshLodAssets.set(entry, lodMeshes);
2011
2045
  this.meshResidencyEpoch += 1;
2012
2046
  return ok(entry);
2013
2047
  }
@@ -2739,7 +2773,7 @@ function freezeRenderFeaturePlan(identity, plan, logicalTargets2 = []) {
2739
2773
  for (const resource of plan.resources) {
2740
2774
  if (!validName(resource.name) || resources.has(resource.name))
2741
2775
  return err$1(stageFailure(identity));
2742
- if (resource.kind === "prepared-gpu-resource" && resource.logicalTarget !== void 0 && !targets.has(resource.logicalTarget)) {
2776
+ if (resource.kind === "prepared-gpu-resource" && resource.logicalTarget !== void 0 && !(typeof resource.logicalTarget === "string" ? targets.has(resource.logicalTarget) : isSceneDataTarget(resource.logicalTarget))) {
2743
2777
  return err$1(stageFailure(identity));
2744
2778
  }
2745
2779
  resources.set(resource.name, resource);
@@ -2761,7 +2795,7 @@ function freezeRenderFeaturePlan(identity, plan, logicalTargets2 = []) {
2761
2795
  );
2762
2796
  const resource = resources.get(entry.resource);
2763
2797
  const validBuffer = resource?.kind === "buffer" && layout?.buffer !== void 0;
2764
- const validPrepared = resource?.kind === "prepared-gpu-resource" && (resource.resource.kind === "buffer" && layout?.buffer !== void 0 || resource.resource.kind === "texture-view" && layout?.texture !== void 0 || resource.resource.kind === "sampler" && layout?.sampler !== void 0);
2798
+ const validPrepared = resource?.kind === "prepared-gpu-resource" && (resource.resource.kind === "buffer" && layout?.buffer !== void 0 || resource.resource.kind === "texture-view" && (layout?.texture !== void 0 || layout?.storageTexture !== void 0) || resource.resource.kind === "sampler" && layout?.sampler !== void 0);
2765
2799
  if (!validBuffer && !validPrepared) {
2766
2800
  return err$1(stageFailure(identity));
2767
2801
  }
@@ -3394,7 +3428,7 @@ function asFeatureError(error, identity, order, stage) {
3394
3428
  if (error instanceof Error && typeof error.code === "string") {
3395
3429
  return error;
3396
3430
  }
3397
- return new RenderFeatureStageFailedError(identity, order, stage, "next-frame");
3431
+ return new RenderFeatureStageFailedError(identity, order, stage, "next-frame", error);
3398
3432
  }
3399
3433
  function featureErrorForSlot(slot, error) {
3400
3434
  switch (error.code) {
@@ -3409,14 +3443,16 @@ function featureErrorForSlot(slot, error) {
3409
3443
  slot.feature.identity,
3410
3444
  slot.order,
3411
3445
  "plan",
3412
- "next-frame"
3446
+ "next-frame",
3447
+ error
3413
3448
  );
3414
3449
  default:
3415
3450
  return new RenderFeatureStageFailedError(
3416
3451
  slot.feature.identity,
3417
3452
  slot.order,
3418
3453
  "plan",
3419
- "next-frame"
3454
+ "next-frame",
3455
+ error
3420
3456
  );
3421
3457
  }
3422
3458
  }
@@ -3444,7 +3480,8 @@ function errorDescriptor(error) {
3444
3480
  featureIdentity: "unknown",
3445
3481
  order: -1,
3446
3482
  stage: "plan",
3447
- recovery: "next-frame"
3483
+ recovery: "next-frame",
3484
+ cause: error
3448
3485
  }
3449
3486
  };
3450
3487
  }
@@ -3532,13 +3569,13 @@ function preparePlanResources(slot, plan, graphics, gpu, targets) {
3532
3569
  }
3533
3570
  if (resource.resource.kind === "texture-view") {
3534
3571
  const logical = resource.logicalTarget === void 0 ? void 0 : targetHandle(resource.logicalTarget, targets);
3535
- if (resource.logicalTarget !== void 0 && (logical === void 0 || !isRenderFeatureTargetHandle(logical))) {
3572
+ if (resource.logicalTarget !== void 0 && (logical === void 0 || typeof logical !== "string" && !isRenderFeatureTargetHandle(logical) && !isSceneDataTarget(logical))) {
3536
3573
  return err$1(planFailure(slot));
3537
3574
  }
3538
3575
  const prepared2 = gpu.prepareTextureView(
3539
3576
  resource.name,
3540
3577
  resource.resource.value,
3541
- logical === void 0 || !isRenderFeatureTargetHandle(logical) ? void 0 : logical
3578
+ logical === void 0 ? void 0 : logical
3542
3579
  );
3543
3580
  if (!prepared2.ok) return prepared2;
3544
3581
  preparedGpuResources.set(resource.name, {
@@ -4184,7 +4221,10 @@ function runRenderFeatureFrame(host, input) {
4184
4221
  "plan",
4185
4222
  () => feature.plan(extracted.value, {
4186
4223
  caps: input.caps,
4187
- frame: { frameNumber: input.frameNumber },
4224
+ frame: {
4225
+ frameNumber: input.frameNumber,
4226
+ ...input.frameSize === void 0 ? {} : input.frameSize
4227
+ },
4188
4228
  generation: transaction.generation,
4189
4229
  targets,
4190
4230
  sceneData,
@@ -4241,9 +4281,7 @@ function runRenderFeatureFrame(host, input) {
4241
4281
  };
4242
4282
  const resolver = input.createPreparedGraphicsResolver?.(resolverInput);
4243
4283
  const resolveGraphics = resolver === void 0 ? void 0 : (descriptor) => {
4244
- const resolved2 = resolveGraphicsSnapshot(resolver, descriptor, transaction.generation);
4245
- if (!resolved2.ok) resolver.release();
4246
- return resolved2;
4284
+ return resolveGraphicsSnapshot(resolver, descriptor, transaction.generation);
4247
4285
  };
4248
4286
  const releaseResolver = () => {
4249
4287
  if (resolver !== void 0) resolver.release();
@@ -4271,9 +4309,7 @@ function runRenderFeatureFrame(host, input) {
4271
4309
  host.setStatus(identity, "failed", slot.latestError);
4272
4310
  continue;
4273
4311
  }
4274
- const graphicsLeases = projected.value.passes.flatMap(
4275
- (pass) => pass.resolvedGraphics?.leases ?? []
4276
- );
4312
+ const graphicsLeases = resolver?.leases ?? projected.value.passes.flatMap((pass) => pass.resolvedGraphics?.leases ?? []);
4277
4313
  if (graphicsLeases.length > 0) requiresPreparedResourceKey = true;
4278
4314
  const leases = [...new Set(graphicsLeases), ...gpu?.retireUntouched() ?? []];
4279
4315
  const retained = host.retainPreparedGraphics(identity, leases);
@@ -4357,6 +4393,66 @@ function createRenderFeatureHost(features, _caps) {
4357
4393
  return ok$1(new FeatureHostImpl(slots));
4358
4394
  }
4359
4395
 
4396
+ // src/features/motion-blur/motion-blur-runtime.ts
4397
+ function motionBlurDrawOptions(input) {
4398
+ return {
4399
+ ...input.sampleTimeSeconds === void 0 ? {} : { sampleTimeSeconds: input.sampleTimeSeconds },
4400
+ ...input.temporalReset === true ? { temporalReset: true } : {}
4401
+ };
4402
+ }
4403
+ function planMotionBlurFrame(input) {
4404
+ const rawSampleDelta = motionBlurSampleDelta(
4405
+ input.sampleTimeSeconds,
4406
+ input.acceptedSampleTimeSeconds,
4407
+ input.fallbackDeltaSeconds
4408
+ );
4409
+ const accepted = input.acceptedTemporal;
4410
+ const reset = input.temporalReset || accepted === void 0 || accepted.historyValid === false && accepted.resetReason === void 0 || !isMotionBlurIntervalValid(rawSampleDelta) || accepted.input.width !== input.width || accepted.input.height !== input.height || accepted.input.viewIdentity !== input.cameraViewIdentity || accepted.input.historyVersion !== input.historyVersion || accepted.input.environmentSignature !== input.environmentSignature || accepted.input.fogSignature !== input.fogSignature || accepted.input.deviceGeneration !== input.deviceGeneration;
4411
+ const params = input.params === void 0 ? void 0 : { ...input.params, targetFps: input.params.targetFps ?? 60 };
4412
+ const demanded = motionBlurTemporalDemand(params);
4413
+ return {
4414
+ rawSampleDelta,
4415
+ demanded,
4416
+ featureInput: params === void 0 ? void 0 : {
4417
+ params,
4418
+ frameDeltaSeconds: rawSampleDelta,
4419
+ reset: demanded && reset
4420
+ }
4421
+ };
4422
+ }
4423
+ function projectMotionBlurInspection(input) {
4424
+ if (input.camera?.motionBlur === void 0 && input.invalidParams === void 0) {
4425
+ return void 0;
4426
+ }
4427
+ const motionBlur = input.camera?.motionBlur === void 0 ? DEFAULT_MOTION_BLUR_PARAMS : { ...input.camera.motionBlur, targetFps: input.camera.motionBlur.targetFps ?? 60 };
4428
+ const demanded = motionBlurTemporalDemand(motionBlur);
4429
+ const unavailable3 = demanded && !input.capabilities.rgba16floatRenderable;
4430
+ const lane = input.capabilities.compute && input.capabilities.storageBuffer && input.capabilities.storageTexture && input.capabilities.rgba16floatRenderable ? "compute" : "raster-limited";
4431
+ const accepted = input.acceptedTemporal;
4432
+ const submitFailure = input.transaction.lastFailure === "queue-submit-failed" || accepted?.resetReason === "submit-failure";
4433
+ const execution = input.execution;
4434
+ const executionAccepted = execution !== void 0 && input.temporalFrameId === execution.frameId && execution.deviceGeneration === input.deviceGeneration && execution.graphGeneration === input.graphGeneration;
4435
+ const active = demanded && !unavailable3 && lane === "compute" && executionAccepted && accepted?.historyValid === true && accepted.resetReason === void 0;
4436
+ return {
4437
+ enabled: demanded,
4438
+ status: input.invalidParams !== void 0 ? "invalid" : !demanded ? "off" : active ? "active" : !unavailable3 && lane === "raster-limited" ? "limited" : "reset",
4439
+ lane,
4440
+ shutterAngle: motionBlur.shutterAngle,
4441
+ maxRadiusPixels: motionBlur.maxRadiusPixels,
4442
+ sampleCount: motionBlur.sampleCount,
4443
+ targetFps: motionBlur.targetFps ?? 60,
4444
+ effectiveSampleCount: !demanded ? 0 : effectiveMotionBlurSampleCount(motionBlur.sampleCount),
4445
+ temporalDemand: demanded ? "scene-data-temporal-v1" : "none",
4446
+ passName: "motion-blur",
4447
+ historyWrites: 0,
4448
+ passCount: !demanded ? 0 : executionAccepted ? 2 : lane === "raster-limited" ? 1 : 0,
4449
+ tapBudget: !demanded ? 0 : effectiveMotionBlurSampleCount(motionBlur.sampleCount),
4450
+ ...executionAccepted ? { execution } : {},
4451
+ ...accepted?.resetReason === void 0 ? {} : { resetReason: accepted.resetReason },
4452
+ ...input.invalidParams !== void 0 ? { lastFailure: "invalid-params" } : unavailable3 ? { lastFailure: "scene-data-unavailable" } : submitFailure ? { lastFailure: "submit-failure" } : {}
4453
+ };
4454
+ }
4455
+
4360
4456
  // src/lifecycle.ts
4361
4457
  var LostListenerRegistry = class {
4362
4458
  listeners = /* @__PURE__ */ new Set();
@@ -4715,8 +4811,8 @@ function addAtmosphereBackground(graph, color) {
4715
4811
  settings.mieDirectionalG,
4716
4812
  settings.sunAngularRadius,
4717
4813
  settings.sunAngularRadius > 0 ? 1 : 0,
4718
- 0,
4719
- 0
4814
+ settings.circumsolarStrength,
4815
+ settings.circumsolarWidth
4720
4816
  ]);
4721
4817
  frame.runtime.device.queue.writeBuffer(resources.buffer(params.value).unwrap(), 0, values).unwrap();
4722
4818
  const triangles = new Float32Array(6 * 9);
@@ -6185,7 +6281,7 @@ var VOLUME_FROXEL_FORMAT = "rgba8unorm";
6185
6281
  var VOLUME_HISTORY_FORMAT = "rgba16float";
6186
6282
  var VOLUME_RESOLVED_FORMAT = "rgba16float";
6187
6283
  var VOLUME_PACKING_Z = 4;
6188
- var VOLUMETRIC_FOG_PARAMS_BYTES = 8 * 16;
6284
+ var VOLUMETRIC_FOG_PARAMS_BYTES = 9 * 16;
6189
6285
  function deriveVolumetricFogExtent(surface, depth = 64, tileSize = 4) {
6190
6286
  const physicalTileSize = Math.max(2, Math.floor(tileSize / 2));
6191
6287
  return {
@@ -6404,7 +6500,10 @@ function importVolumeInputs(graph, topology, linearHdr, sceneDepth, directionalS
6404
6500
  if (!density.ok) return density;
6405
6501
  const params = graph.importBuffer(
6406
6502
  "volume-params",
6407
- { size: 128, usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST },
6503
+ {
6504
+ size: VOLUMETRIC_FOG_PARAMS_BYTES,
6505
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST
6506
+ },
6408
6507
  (frame) => {
6409
6508
  const context = frame.volumetricFog;
6410
6509
  if (context === void 0) throw pipelineError2("parameter residency");
@@ -6640,9 +6739,9 @@ function addVolumetricFogPasses(graph, inputs) {
6640
6739
  const densitySampler = frame.runtime.device.createSampler({
6641
6740
  minFilter: "linear",
6642
6741
  magFilter: "linear",
6643
- addressModeU: "clamp-to-edge",
6644
- addressModeV: "clamp-to-edge",
6645
- addressModeW: "clamp-to-edge"
6742
+ addressModeU: "repeat",
6743
+ addressModeV: "repeat",
6744
+ addressModeW: "repeat"
6646
6745
  });
6647
6746
  if (!densitySampler.ok) throw densitySampler.error;
6648
6747
  const bindings = volumeBindGroup(frame, integrate.layout, [
@@ -6973,6 +7072,1546 @@ function addMotionBlurPass(graph, input, temporal, output) {
6973
7072
  output
6974
7073
  });
6975
7074
  }
7075
+ var MOTION_BLUR_COMPUTE_WGSL = `struct MotionBlurComputeParams {
7076
+ shutterAngle : f32,
7077
+ maxRadiusPixels : f32,
7078
+ sampleCount : u32,
7079
+ reset : u32,
7080
+ targetFps : f32,
7081
+ exposureScale : f32,
7082
+ frameDeltaSeconds : f32,
7083
+ flags : u32,
7084
+ };
7085
+
7086
+ struct MotionBlurTileSummary {
7087
+ // The two motion lanes retain the source pixel that supplied the direction.
7088
+ // Those anchors let a bounded midpoint tap hit a sparse source without
7089
+ // adding a color read or growing the 32-byte per-tile summary.
7090
+ maxMotionAndLength : vec4<f32>,
7091
+ secondaryMotionAndDepth : vec4<f32>,
7092
+ };
7093
+
7094
+ struct TemporalSample {
7095
+ motion : vec2<f32>,
7096
+ depth : f32,
7097
+ reactive : f32,
7098
+ validDepth : bool,
7099
+ motionValid : bool,
7100
+ };
7101
+
7102
+ struct MotionBlurSupport {
7103
+ primaryMotion : vec2<f32>,
7104
+ secondaryMotion : vec2<f32>,
7105
+ primaryAnchor : vec2<i32>,
7106
+ secondaryAnchor : vec2<i32>,
7107
+ primaryRadius : f32,
7108
+ secondaryRadius : f32,
7109
+ valid : bool,
7110
+ longVector : bool,
7111
+ };
7112
+
7113
+ struct MotionBlurTap {
7114
+ pixel : vec2<i32>,
7115
+ fallbackPixel : vec2<i32>,
7116
+ valid : bool,
7117
+ fallbackValid : bool,
7118
+ };
7119
+
7120
+ struct MotionBlurFilter {
7121
+ color0 : vec4<f32>,
7122
+ color1 : vec4<f32>,
7123
+ color2 : vec4<f32>,
7124
+ color3 : vec4<f32>,
7125
+ depth0 : f32,
7126
+ depth1 : f32,
7127
+ depth2 : f32,
7128
+ depth3 : f32,
7129
+ coverage0 : f32,
7130
+ coverage1 : f32,
7131
+ coverage2 : f32,
7132
+ coverage3 : f32,
7133
+ receiver : vec2<i32>,
7134
+ motion : vec2<f32>,
7135
+ radius : f32,
7136
+ receiverMask : u32,
7137
+ validMask : u32,
7138
+ valid : bool,
7139
+ };
7140
+
7141
+ struct MotionBlurCacheSample {
7142
+ color : vec4<f32>,
7143
+ coverage : f32,
7144
+ valid : bool,
7145
+ };
7146
+
7147
+ struct MotionBlurSource {
7148
+ pixel : vec2<i32>,
7149
+ motion : vec2<f32>,
7150
+ depth : f32,
7151
+ radius : f32,
7152
+ coverage0 : f32,
7153
+ coverage1 : f32,
7154
+ coverage2 : f32,
7155
+ coverage3 : f32,
7156
+ receiverMask : u32,
7157
+ valid : bool,
7158
+ };
7159
+
7160
+ @group(0) @binding(0) var currentColor : texture_2d<f32>;
7161
+ @group(0) @binding(1) var sceneTemporal : texture_2d<f32>;
7162
+ @group(0) @binding(2) var<storage, read_write> tileSummary : array<MotionBlurTileSummary>;
7163
+ @group(0) @binding(3) var outputColor : texture_storage_2d<rgba16float, write>;
7164
+ @group(0) @binding(4) var<uniform> params : MotionBlurComputeParams;
7165
+
7166
+ // Reconstruct a bounded part of a partially covered opaque edge and fund it
7167
+ // by attenuating the matching empty-background trail. The fixed factor keeps
7168
+ // the work and the energy adjustment independent of the blur radius.
7169
+ const EDGE_FILL_FACTOR : f32 = 0.99;
7170
+
7171
+ // One 8x8 workgroup covers a 16x16 tile. Each invocation reads a 2x2
7172
+ // footprint, then the workgroup reduction combines the values without
7173
+ // write contention or a serial 256-pixel loop.
7174
+ var<workgroup> tileMotionScratch : array<vec2<f32>, 64>;
7175
+ var<workgroup> tileLengthScratch : array<f32, 64>;
7176
+ var<workgroup> tileSourceScratch : array<vec2<i32>, 64>;
7177
+ var<workgroup> tileSecondaryMotionScratch : array<vec2<f32>, 64>;
7178
+ var<workgroup> tileSecondaryLengthScratch : array<f32, 64>;
7179
+ var<workgroup> tileSecondarySourceScratch : array<vec2<i32>, 64>;
7180
+ // The reconstruction workgroup owns a 4x4 half-resolution result cache for
7181
+ // its 8x8 output footprint. Each owner computes the complete bounded support
7182
+ // filter once. Full-resolution lanes only select a depth-compatible cached
7183
+ // result after the uniform barrier, with no cross-workgroup dependency.
7184
+ // Four quadrants are kept per half-cell. Each owner pays four receiver
7185
+ // center-color reads and at most supportCount (<=15) accepted source-color
7186
+ // reads; midpoint anchor recovery reuses an existing slot and adds no read
7187
+ // budget. The shared result retains odd/even coverage instead of spreading an
7188
+ // odd row across both rows.
7189
+ var<workgroup> halfFilteredColor : array<vec4<f32>, 64>;
7190
+ var<workgroup> halfFilteredDepth : array<f32, 64>;
7191
+ var<workgroup> halfFilteredCoverage : array<f32, 64>;
7192
+ var<workgroup> halfFilteredReceiver : array<vec2<i32>, 16>;
7193
+ var<workgroup> halfFilteredMotion : array<vec2<f32>, 16>;
7194
+ var<workgroup> halfFilteredRadius : array<f32, 16>;
7195
+ var<workgroup> halfFilteredValid : array<u32, 64>;
7196
+ var<workgroup> longVectorScratch : array<u32, 64>;
7197
+ // A reconstruct workgroup covers one 8x8 output footprint. Its first lane
7198
+ // derives the complete bounded tile neighbourhood once and publishes the top
7199
+ // two source directions for all lanes and all designated half-resolution
7200
+ // owners. This keeps summary work O(P/T^2), rather than repeating a tile walk
7201
+ // per output pixel.
7202
+ var<workgroup> neighbourhoodPrimaryMotion : vec2<f32>;
7203
+ var<workgroup> neighbourhoodPrimaryLength : f32;
7204
+ var<workgroup> neighbourhoodPrimaryAnchor : vec2<i32>;
7205
+ var<workgroup> neighbourhoodSecondaryMotion : vec2<f32>;
7206
+ var<workgroup> neighbourhoodSecondaryLength : f32;
7207
+ var<workgroup> neighbourhoodSecondaryAnchor : vec2<i32>;
7208
+
7209
+ // maxRadiusPixels is authored at [0,64], so a 16-pixel tile support reaches
7210
+ // at most four tiles. The fixed 9x9 lattice is geometrically derived from its
7211
+ // index, then intersected with the circular/AABB support. It is a constant
7212
+ // bounded metadata walk: no loop scales with the requested radius and no
7213
+ // radius-squared per-pixel storage or write path is introduced.
7214
+ const MAX_TILE_REACH : i32 = 4;
7215
+ const TILE_NEIGHBORHOOD_SIDE : u32 = 9u;
7216
+ const TILE_NEIGHBORHOOD_COUNT : u32 = 81u;
7217
+
7218
+ fn finite(value : f32) -> bool {
7219
+ return value == value && abs(value) < 3.402823e+38;
7220
+ }
7221
+
7222
+ fn pixelInBounds(pixel : vec2<i32>, dimensions : vec2<i32>) -> bool {
7223
+ return pixel.x >= 0 && pixel.y >= 0 && pixel.x < dimensions.x && pixel.y < dimensions.y;
7224
+ }
7225
+
7226
+ fn decodeDepth(packed : f32) -> f32 {
7227
+ if (packed < 0.0) { return 0.0; }
7228
+ return exp2(packed) - 1.0;
7229
+ }
7230
+
7231
+ fn temporalAt(pixel : vec2<i32>, dimensions : vec2<i32>) -> TemporalSample {
7232
+ let samplePixel = clamp(pixel, vec2<i32>(0), dimensions - vec2<i32>(1));
7233
+ let packed = textureLoad(sceneTemporal, samplePixel, 0);
7234
+ let invalidMotion = packed.w >= 2.0;
7235
+ return TemporalSample(
7236
+ packed.xy,
7237
+ decodeDepth(packed.z),
7238
+ clamp(packed.w - select(0.0, 2.0, invalidMotion), 0.0, 1.0),
7239
+ packed.z >= 0.0,
7240
+ !invalidMotion,
7241
+ );
7242
+ }
7243
+
7244
+ fn depthReject(center : f32, sample : f32) -> bool {
7245
+ return sample > center + max(0.01, center * 0.01);
7246
+ }
7247
+
7248
+ fn distinctDirection(a : vec2<f32>, b : vec2<f32>) -> bool {
7249
+ let aLength = length(a);
7250
+ let bLength = length(b);
7251
+ if (aLength <= 1e-5 || bLength <= 1e-5) { return false; }
7252
+ // Keep only genuinely different directions. Opposite signs still carry
7253
+ // separate source segments, while nearly parallel candidates do not spend
7254
+ // the bounded second-direction slot.
7255
+ return abs(dot(a / aLength, b / bLength)) < 0.94;
7256
+ }
7257
+
7258
+ fn sourceVelocitySegmentCovers(
7259
+ receiverPixel : vec2<i32>,
7260
+ sourcePixel : vec2<i32>,
7261
+ motionPixels : vec2<f32>,
7262
+ radius : f32,
7263
+ ) -> bool {
7264
+ let delta = vec2<f32>(receiverPixel - sourcePixel);
7265
+ let motionLength = length(motionPixels);
7266
+ if (motionLength <= 1e-5 || radius <= 1e-5) { return false; }
7267
+ let direction = motionPixels / motionLength;
7268
+ let along = dot(delta, direction);
7269
+ let perpendicular = abs(delta.x * direction.y - delta.y * direction.x);
7270
+ // A source segment owns a one-pixel coverage tube. This is a bounded
7271
+ // geometric test, not a radius-sized source scan, and lets a sparse mover
7272
+ // cover a static receiver without accepting a far/orthogonal source.
7273
+ return abs(along) <= radius + 0.75 && perpendicular <= 0.75;
7274
+ }
7275
+
7276
+ fn symmetricOffset(index : u32, count : u32, motion : vec2<f32>) -> vec2<f32> {
7277
+ let denominator = max(f32(count), 1.0);
7278
+ return motion * (2.0 * (f32(index) + 0.5) / denominator - 1.0);
7279
+ }
7280
+
7281
+ fn tileNeighborhoodOffset(index : u32) -> vec2<i32> {
7282
+ let x = i32(index % TILE_NEIGHBORHOOD_SIDE) - MAX_TILE_REACH;
7283
+ let y = i32(index / TILE_NEIGHBORHOOD_SIDE) - MAX_TILE_REACH;
7284
+ return vec2<i32>(x, y);
7285
+ }
7286
+
7287
+ fn tileOffsetWithinSupport(offset : vec2<i32>, tileReach : i32) -> bool {
7288
+ if (offset.x == 0 && offset.y == 0) { return false; }
7289
+ let boundedReach = min(max(tileReach, 1), MAX_TILE_REACH);
7290
+ // The bounded lattice is a square around the receiver tile. The precise
7291
+ // AABB/circle intersection below decides whether a diagonal tile really
7292
+ // intersects the support; a circular pre-filter here would drop the
7293
+ // (\xB12,\xB12) and boundary cases before that exact test can run.
7294
+ return abs(offset.x) <= boundedReach && abs(offset.y) <= boundedReach;
7295
+ }
7296
+
7297
+ fn tileIntersectsSupport(
7298
+ receiverPixel : vec2<i32>,
7299
+ candidateTile : vec2<i32>,
7300
+ dimensions : vec2<i32>,
7301
+ supportRadius : f32,
7302
+ ) -> bool {
7303
+ // Use the receiver-to-tile-AABB lower bound. This includes every tile whose
7304
+ // contents can intersect the support, including signs and diagonals, while
7305
+ // rejecting an outside-support tile before loading its summary.
7306
+ let tileMin = vec2<f32>(candidateTile * 16);
7307
+ let tileMax = min(tileMin + vec2<f32>(16.0), vec2<f32>(dimensions));
7308
+ let receiver = vec2<f32>(receiverPixel) + vec2<f32>(0.5);
7309
+ let dx = max(max(tileMin.x - receiver.x, 0.0), receiver.x - tileMax.x);
7310
+ let dy = max(max(tileMin.y - receiver.y, 0.0), receiver.y - tileMax.y);
7311
+ let distance = vec2<f32>(dx, dy);
7312
+ return dot(distance, distance) <= supportRadius * supportRadius + 1.0;
7313
+ }
7314
+
7315
+ fn tileIntersectsWorkgroupSupport(
7316
+ workgroupBase : vec2<i32>,
7317
+ candidateTile : vec2<i32>,
7318
+ dimensions : vec2<i32>,
7319
+ supportRadius : f32,
7320
+ ) -> bool {
7321
+ // A workgroup's 8x8 rectangle is the conservative receiver domain for one
7322
+ // shared metadata walk. The AABB lower bound includes every pixel in that
7323
+ // rectangle that can reach the candidate tile, including diagonal/sign
7324
+ // variants, while rejecting a tile fully outside the support.
7325
+ let tileMin = vec2<f32>(candidateTile * 16);
7326
+ let tileMax = min(tileMin + vec2<f32>(16.0), vec2<f32>(dimensions));
7327
+ let groupMin = vec2<f32>(workgroupBase) + vec2<f32>(0.5);
7328
+ let groupMax = min(groupMin + vec2<f32>(8.0), vec2<f32>(dimensions));
7329
+ let dx = max(max(tileMin.x - groupMax.x, 0.0), groupMin.x - tileMax.x);
7330
+ let dy = max(max(tileMin.y - groupMax.y, 0.0), groupMin.y - tileMax.y);
7331
+ let distance = vec2<f32>(dx, dy);
7332
+ return dot(distance, distance) <= supportRadius * supportRadius + 1.0;
7333
+ }
7334
+
7335
+ fn scanTileNeighbourhood(
7336
+ workgroupBase : vec2<i32>,
7337
+ dimensions : vec2<i32>,
7338
+ ) {
7339
+ let tiles = (vec2<u32>(dimensions) + vec2<u32>(15)) / vec2<u32>(16);
7340
+ let tileCoordinate = workgroupBase / vec2<i32>(16);
7341
+ let currentIndex = u32(tileCoordinate.y) * tiles.x + u32(tileCoordinate.x);
7342
+ let current = tileSummary[currentIndex];
7343
+ var primary = current.maxMotionAndLength.xy * vec2<f32>(dimensions) * params.exposureScale;
7344
+ var primaryLength = length(primary);
7345
+ var primaryAnchor = vec2<i32>(
7346
+ i32(current.maxMotionAndLength.z),
7347
+ i32(current.maxMotionAndLength.w),
7348
+ );
7349
+ var secondary = current.secondaryMotionAndDepth.xy * vec2<f32>(dimensions) * params.exposureScale;
7350
+ var secondaryLength = length(secondary);
7351
+ var secondaryAnchor = vec2<i32>(
7352
+ i32(current.secondaryMotionAndDepth.z),
7353
+ i32(current.secondaryMotionAndDepth.w),
7354
+ );
7355
+ let supportRadius = min(max(params.maxRadiusPixels, 1.0), 64.0);
7356
+ let tileReach = min(MAX_TILE_REACH, max(1, i32(ceil(supportRadius / 16.0))));
7357
+ for (var tileCandidateIndex = 0u; tileCandidateIndex < TILE_NEIGHBORHOOD_COUNT; tileCandidateIndex += 1u) {
7358
+ let offset = tileNeighborhoodOffset(tileCandidateIndex);
7359
+ if (!tileOffsetWithinSupport(offset, tileReach)) { continue; }
7360
+ let candidateTile = tileCoordinate + offset;
7361
+ if (
7362
+ candidateTile.x < 0 ||
7363
+ candidateTile.y < 0 ||
7364
+ candidateTile.x >= i32(tiles.x) ||
7365
+ candidateTile.y >= i32(tiles.y) ||
7366
+ !tileIntersectsWorkgroupSupport(workgroupBase, candidateTile, dimensions, supportRadius)
7367
+ ) { continue; }
7368
+ let candidate = tileSummary[u32(candidateTile.y) * tiles.x + u32(candidateTile.x)];
7369
+ let candidatePrimary = candidate.maxMotionAndLength.xy * vec2<f32>(dimensions) * params.exposureScale;
7370
+ let candidatePrimaryLength = length(candidatePrimary);
7371
+ let candidatePrimaryAnchor = vec2<i32>(
7372
+ i32(candidate.maxMotionAndLength.z),
7373
+ i32(candidate.maxMotionAndLength.w),
7374
+ );
7375
+ if (candidatePrimaryLength > primaryLength) {
7376
+ let previousPrimary = primary;
7377
+ let previousLength = primaryLength;
7378
+ let previousAnchor = primaryAnchor;
7379
+ primary = candidatePrimary;
7380
+ primaryLength = candidatePrimaryLength;
7381
+ primaryAnchor = candidatePrimaryAnchor;
7382
+ if (previousLength > 1e-5 && distinctDirection(primary, previousPrimary) && previousLength > secondaryLength) {
7383
+ secondary = previousPrimary;
7384
+ secondaryLength = previousLength;
7385
+ secondaryAnchor = previousAnchor;
7386
+ }
7387
+ } else if (
7388
+ candidatePrimaryLength > secondaryLength &&
7389
+ distinctDirection(primary, candidatePrimary)
7390
+ ) {
7391
+ secondary = candidatePrimary;
7392
+ secondaryLength = candidatePrimaryLength;
7393
+ secondaryAnchor = candidatePrimaryAnchor;
7394
+ }
7395
+ let candidateSecondary = candidate.secondaryMotionAndDepth.xy * vec2<f32>(dimensions) * params.exposureScale;
7396
+ let candidateSecondaryLength = length(candidateSecondary);
7397
+ let candidateSecondaryAnchor = vec2<i32>(
7398
+ i32(candidate.secondaryMotionAndDepth.z),
7399
+ i32(candidate.secondaryMotionAndDepth.w),
7400
+ );
7401
+ if (candidateSecondaryLength > secondaryLength && distinctDirection(primary, candidateSecondary)) {
7402
+ secondary = candidateSecondary;
7403
+ secondaryLength = candidateSecondaryLength;
7404
+ secondaryAnchor = candidateSecondaryAnchor;
7405
+ }
7406
+ }
7407
+ neighbourhoodPrimaryMotion = primary;
7408
+ neighbourhoodPrimaryLength = primaryLength;
7409
+ neighbourhoodPrimaryAnchor = primaryAnchor;
7410
+ neighbourhoodSecondaryMotion = secondary;
7411
+ neighbourhoodSecondaryLength = secondaryLength;
7412
+ neighbourhoodSecondaryAnchor = secondaryAnchor;
7413
+ }
7414
+
7415
+ fn validMotionParameters() -> bool {
7416
+ return
7417
+ finite(params.frameDeltaSeconds) &&
7418
+ params.frameDeltaSeconds > 0.0 &&
7419
+ params.frameDeltaSeconds <= 0.1 &&
7420
+ finite(params.exposureScale) &&
7421
+ params.exposureScale > 0.0 &&
7422
+ finite(params.targetFps) &&
7423
+ params.targetFps >= 0.0 &&
7424
+ params.targetFps <= 240.0 &&
7425
+ params.targetFps == floor(params.targetFps) &&
7426
+ params.sampleCount >= 4u &&
7427
+ params.maxRadiusPixels > 0.0 &&
7428
+ finite(params.maxRadiusPixels) &&
7429
+ params.shutterAngle > 0.0;
7430
+ }
7431
+
7432
+ fn sourceMotionPixels(sample : TemporalSample, dimensions : vec2<i32>) -> vec2<f32> {
7433
+ return sample.motion * vec2<f32>(dimensions) * params.exposureScale;
7434
+ }
7435
+
7436
+ fn sourceRadius(motionPixels : vec2<f32>, shutter : f32) -> f32 {
7437
+ return min(length(motionPixels) * shutter * 0.5, params.maxRadiusPixels);
7438
+ }
7439
+
7440
+ fn resolveMotionBlurSupport(
7441
+ pixel : vec2<i32>,
7442
+ dimensions : vec2<i32>,
7443
+ ) -> MotionBlurSupport {
7444
+ var empty = MotionBlurSupport(
7445
+ vec2<f32>(0.0),
7446
+ vec2<f32>(0.0),
7447
+ vec2<i32>(0),
7448
+ vec2<i32>(0),
7449
+ 0.0,
7450
+ 0.0,
7451
+ false,
7452
+ false,
7453
+ );
7454
+ if (!pixelInBounds(pixel, dimensions) || !validMotionParameters() || params.reset != 0u) {
7455
+ return empty;
7456
+ }
7457
+ let center = temporalAt(pixel, dimensions);
7458
+ if (!center.validDepth || !center.motionValid) { return empty; }
7459
+ let shutter = clamp(params.shutterAngle / 360.0, 0.0, 1.0);
7460
+ let centerMotion = sourceMotionPixels(center, dimensions);
7461
+ let centerLength = length(centerMotion);
7462
+ var primaryMotion = centerMotion;
7463
+ var primaryLength = centerLength;
7464
+ var primaryAnchor = pixel;
7465
+ var secondaryMotion = neighbourhoodSecondaryMotion;
7466
+ var secondaryLength = neighbourhoodSecondaryLength;
7467
+ var secondaryAnchor = neighbourhoodSecondaryAnchor;
7468
+ if (neighbourhoodPrimaryLength > primaryLength) {
7469
+ if (distinctDirection(primaryMotion, neighbourhoodPrimaryMotion)) {
7470
+ secondaryMotion = primaryMotion;
7471
+ secondaryLength = primaryLength;
7472
+ secondaryAnchor = pixel;
7473
+ }
7474
+ primaryMotion = neighbourhoodPrimaryMotion;
7475
+ primaryLength = neighbourhoodPrimaryLength;
7476
+ primaryAnchor = neighbourhoodPrimaryAnchor;
7477
+ } else if (
7478
+ neighbourhoodPrimaryLength > secondaryLength &&
7479
+ distinctDirection(primaryMotion, neighbourhoodPrimaryMotion)
7480
+ ) {
7481
+ secondaryMotion = neighbourhoodPrimaryMotion;
7482
+ secondaryLength = neighbourhoodPrimaryLength;
7483
+ secondaryAnchor = neighbourhoodPrimaryAnchor;
7484
+ }
7485
+ if (
7486
+ secondaryLength <= 1e-5 ||
7487
+ !distinctDirection(primaryMotion, secondaryMotion)
7488
+ ) {
7489
+ secondaryMotion = vec2<f32>(0.0);
7490
+ secondaryLength = 0.0;
7491
+ }
7492
+ empty.primaryMotion = primaryMotion;
7493
+ empty.secondaryMotion = secondaryMotion;
7494
+ empty.primaryAnchor = primaryAnchor;
7495
+ empty.secondaryAnchor = secondaryAnchor;
7496
+ empty.primaryRadius = sourceRadius(primaryMotion, shutter);
7497
+ empty.secondaryRadius = sourceRadius(secondaryMotion, shutter);
7498
+ empty.valid = empty.primaryRadius > 1e-5 || empty.secondaryRadius > 1e-5;
7499
+ empty.longVector = max(empty.primaryRadius, empty.secondaryRadius) > 8.0;
7500
+ return empty;
7501
+ }
7502
+
7503
+ fn sourceAnchorForReceiver(
7504
+ receiverPixel : vec2<i32>,
7505
+ anchor : vec2<i32>,
7506
+ motion : vec2<f32>,
7507
+ dimensions : vec2<i32>,
7508
+ ) -> vec2<i32> {
7509
+ // A tile summary has one source anchor, while a horizontal source may span
7510
+ // the tile's perpendicular axis. Project that anchor onto the receiver's
7511
+ // motion line before the midpoint read. The following source footprint
7512
+ // validates the projected pixel, so a thin or off-axis source cannot be
7513
+ // invented by the projection.
7514
+ let motionLength = length(motion);
7515
+ if (motionLength <= 1e-5) { return anchor; }
7516
+ let direction = motion / motionLength;
7517
+ let delta = vec2<f32>(receiverPixel - anchor);
7518
+ let perpendicular = delta - direction * dot(delta, direction);
7519
+ let projected = vec2<f32>(anchor) + perpendicular;
7520
+ let candidate = vec2<i32>(floor(projected + vec2<f32>(0.5)));
7521
+ return clamp(candidate, vec2<i32>(0), dimensions - vec2<i32>(1));
7522
+ }
7523
+
7524
+ fn supportTap(
7525
+ receiverPixel : vec2<i32>,
7526
+ index : u32,
7527
+ count : u32,
7528
+ motion : vec2<f32>,
7529
+ radius : f32,
7530
+ anchor : vec2<i32>,
7531
+ dimensions : vec2<i32>,
7532
+ ) -> MotionBlurTap {
7533
+ // Preserve the geometric midpoint as the primary sample. The source anchor
7534
+ // is a recovery coordinate for a sparse tile summary, not a replacement for
7535
+ // the receiver's authored support. It is tried only when this midpoint has
7536
+ // no valid source, and therefore never adds a color read or a tap.
7537
+ var fallbackPixel = vec2<i32>(0);
7538
+ var fallbackValid = false;
7539
+ if (index == count / 2u) {
7540
+ fallbackPixel = sourceAnchorForReceiver(receiverPixel, anchor, motion, dimensions);
7541
+ fallbackValid = pixelInBounds(fallbackPixel, dimensions);
7542
+ }
7543
+ let direction = motion / max(length(motion), 1e-5);
7544
+ let offset = symmetricOffset(index, count, direction * radius);
7545
+ let dimensionsF = vec2<f32>(dimensions);
7546
+ let uv = (vec2<f32>(receiverPixel) + vec2<f32>(0.5) + offset) / dimensionsF;
7547
+ if (uv.x < 0.0 || uv.y < 0.0 || uv.x >= 1.0 || uv.y >= 1.0) {
7548
+ return MotionBlurTap(vec2<i32>(0), fallbackPixel, false, fallbackValid);
7549
+ }
7550
+ let candidate = vec2<i32>(floor(uv * dimensionsF));
7551
+ return MotionBlurTap(candidate, fallbackPixel, pixelInBounds(candidate, dimensions), fallbackValid);
7552
+ }
7553
+
7554
+ fn sourceAt(
7555
+ receiverPixel : vec2<i32>,
7556
+ sourcePixel : vec2<i32>,
7557
+ centerDepth : f32,
7558
+ shutter : f32,
7559
+ dimensions : vec2<i32>,
7560
+ ) -> MotionBlurSource {
7561
+ let empty = MotionBlurSource(
7562
+ vec2<i32>(0),
7563
+ vec2<f32>(0.0),
7564
+ 0.0,
7565
+ 0.0,
7566
+ 0.0,
7567
+ 0.0,
7568
+ 0.0,
7569
+ 0.0,
7570
+ 0u,
7571
+ false,
7572
+ );
7573
+ if (!pixelInBounds(sourcePixel, dimensions)) { return empty; }
7574
+ let source = temporalAt(sourcePixel, dimensions);
7575
+ if (!source.validDepth || !source.motionValid || depthReject(centerDepth, source.depth)) {
7576
+ return empty;
7577
+ }
7578
+ let motionPixels = sourceMotionPixels(source, dimensions);
7579
+ let radius = sourceRadius(motionPixels, shutter);
7580
+ if (!sourceVelocitySegmentCovers(receiverPixel, sourcePixel, motionPixels, radius)) {
7581
+ return empty;
7582
+ }
7583
+ return MotionBlurSource(
7584
+ sourcePixel,
7585
+ motionPixels,
7586
+ source.depth,
7587
+ radius,
7588
+ 1.0,
7589
+ 1.0,
7590
+ 1.0,
7591
+ 1.0,
7592
+ 0u,
7593
+ true,
7594
+ );
7595
+ }
7596
+
7597
+ // A half-resolution owner represents a complete 2x2 receiving footprint. The
7598
+ // source motion test is evaluated against every receiving pixel in that
7599
+ // footprint, so odd/even rows and columns remain visible without a second
7600
+ // filter pass or an unbounded neighbourhood walk.
7601
+ fn sourceAtFootprint(
7602
+ receiverPixel : vec2<i32>,
7603
+ sourcePixel : vec2<i32>,
7604
+ centerDepth : f32,
7605
+ shutter : f32,
7606
+ dimensions : vec2<i32>,
7607
+ ) -> MotionBlurSource {
7608
+ var selected = MotionBlurSource(
7609
+ vec2<i32>(0),
7610
+ vec2<f32>(0.0),
7611
+ 0.0,
7612
+ 0.0,
7613
+ 0.0,
7614
+ 0.0,
7615
+ 0.0,
7616
+ 0.0,
7617
+ 0u,
7618
+ false,
7619
+ );
7620
+ var selectedDepth = 3.402823e+38;
7621
+ var coverage0 = 0.0;
7622
+ var coverage1 = 0.0;
7623
+ var coverage2 = 0.0;
7624
+ var coverage3 = 0.0;
7625
+ var selectedReceiverMask = 0u;
7626
+ for (var y = 0u; y < 2u; y += 1u) {
7627
+ for (var x = 0u; x < 2u; x += 1u) {
7628
+ let candidatePixel = sourcePixel + vec2<i32>(i32(x), i32(y));
7629
+ if (!pixelInBounds(candidatePixel, dimensions)) { continue; }
7630
+ let temporal = temporalAt(candidatePixel, dimensions);
7631
+ if (!temporal.validDepth || !temporal.motionValid || depthReject(centerDepth, temporal.depth)) {
7632
+ continue;
7633
+ }
7634
+ let motionPixels = sourceMotionPixels(temporal, dimensions);
7635
+ let radius = sourceRadius(motionPixels, shutter);
7636
+ var receiverMask = 0u;
7637
+ // A source half-cell and a receiver half-cell share the same integer
7638
+ // displacement. This one-to-one correspondence keeps a diagonal source
7639
+ // from being counted for all four receiver quadrants just because its
7640
+ // tube crosses the quadrant. The segment test still supplies the motion
7641
+ // support and depth/validity checks remain source-owned.
7642
+ let receiver = receiverPixel + vec2<i32>(i32(x), i32(y));
7643
+ if (
7644
+ pixelInBounds(receiver, dimensions) &&
7645
+ sourceVelocitySegmentCovers(receiver, candidatePixel, motionPixels, radius)
7646
+ ) {
7647
+ receiverMask = receiverMask | (1u << (y * 2u + x));
7648
+ }
7649
+ if (receiverMask == 0u) { continue; }
7650
+ if (!selected.valid || temporal.depth < selectedDepth) {
7651
+ selected = MotionBlurSource(
7652
+ candidatePixel,
7653
+ motionPixels,
7654
+ temporal.depth,
7655
+ radius,
7656
+ 0.0,
7657
+ 0.0,
7658
+ 0.0,
7659
+ 0.0,
7660
+ receiverMask,
7661
+ true,
7662
+ );
7663
+ selectedDepth = temporal.depth;
7664
+ coverage0 = 0.0;
7665
+ coverage1 = 0.0;
7666
+ coverage2 = 0.0;
7667
+ coverage3 = 0.0;
7668
+ selectedReceiverMask = receiverMask;
7669
+ }
7670
+ if (abs(temporal.depth - selectedDepth) <= 1e-4) {
7671
+ // The common integer displacement above gives each source cell one
7672
+ // receiver quadrant. The matching footprint therefore contributes its
7673
+ // full unit overlap to that one owner.
7674
+ if ((receiverMask & 1u) != 0u) { coverage0 += 1.0; }
7675
+ if ((receiverMask & 2u) != 0u) { coverage1 += 1.0; }
7676
+ if ((receiverMask & 4u) != 0u) { coverage2 += 1.0; }
7677
+ if ((receiverMask & 8u) != 0u) { coverage3 += 1.0; }
7678
+ selectedReceiverMask = selectedReceiverMask | receiverMask;
7679
+ }
7680
+ }
7681
+ }
7682
+ if (selected.valid) {
7683
+ // The owner samples a 2x2 source footprint but pays for one color read.
7684
+ // Each receiving quadrant keeps the fraction of that footprint that is
7685
+ // covered at the selected depth. This preserves energy at boundaries and
7686
+ // carries a one-pixel odd-row feature into its real row only.
7687
+ selected.coverage0 = min(coverage0, 1.0) * select(0.0, 1.0, (selectedReceiverMask & 1u) != 0u);
7688
+ selected.coverage1 = min(coverage1, 1.0) * select(0.0, 1.0, (selectedReceiverMask & 2u) != 0u);
7689
+ selected.coverage2 = min(coverage2, 1.0) * select(0.0, 1.0, (selectedReceiverMask & 4u) != 0u);
7690
+ selected.coverage3 = min(coverage3, 1.0) * select(0.0, 1.0, (selectedReceiverMask & 8u) != 0u);
7691
+ selected.receiverMask = selectedReceiverMask;
7692
+ }
7693
+ return selected;
7694
+ }
7695
+
7696
+ fn midpointTileOwnership(
7697
+ receiverPixel : vec2<i32>,
7698
+ source : MotionBlurSource,
7699
+ ) -> f32 {
7700
+ // A recovered tile anchor represents one two-pixel source footprint. The
7701
+ // footprint is split across the finite, geometrically derived tile bands of
7702
+ // the support segment. One half-cell owner per band claims the represented
7703
+ // midpoint slot, rather than painting every receiver in the interval.
7704
+ let sourcePixel = source.pixel;
7705
+ let motionLength = length(source.motion);
7706
+ if (motionLength <= 1e-5 || source.radius <= 1e-5) { return 0.0; }
7707
+ let receiverTile = receiverPixel / vec2<i32>(16);
7708
+ let sourceTile = sourcePixel / vec2<i32>(16);
7709
+ let tileDelta = vec2<f32>(sourceTile - receiverTile);
7710
+ let direction = source.motion / motionLength;
7711
+ let along = abs(dot(tileDelta, direction));
7712
+ let perpendicular = abs(tileDelta.x * direction.y - tileDelta.y * direction.x);
7713
+ let tileReach = min(4, max(1, i32(ceil(source.radius / 16.0))));
7714
+ if (along < 1.0 || ceil(along) > f32(tileReach) || perpendicular > 1.0) { return 0.0; }
7715
+ let groupBase = (receiverPixel / vec2<i32>(8)) * vec2<i32>(8);
7716
+ let local = receiverPixel - groupBase;
7717
+ let ownerX = select(
7718
+ clamp(((sourcePixel.x - groupBase.x) / 2) * 2, 0, 6),
7719
+ select(0, 6, sourcePixel.x > receiverPixel.x),
7720
+ sourcePixel.x != receiverPixel.x,
7721
+ );
7722
+ let ownerY = select(
7723
+ clamp(((sourcePixel.y - groupBase.y) / 2) * 2, 0, 6),
7724
+ select(0, 6, sourcePixel.y > receiverPixel.y),
7725
+ sourcePixel.y != receiverPixel.y,
7726
+ );
7727
+ if (local.x != ownerX || local.y != ownerY) { return 0.0; }
7728
+ let sourceFootprintLength = 2.0;
7729
+ let supportLength = max(2.0 * source.radius, sourceFootprintLength);
7730
+ // Divide the finite footprint ownership over every reachable tile band.
7731
+ // This keeps the total recovery mass bounded when the support grows from
7732
+ // 32 to 64 pixels while retaining a nonzero owner at any in-view band.
7733
+ return min(1.0, sourceFootprintLength / (supportLength * f32(tileReach)));
7734
+ }
7735
+
7736
+ fn integrateLongColor(
7737
+ filtered : vec4<f32>,
7738
+ validCount : f32,
7739
+ hasAcceptedSource : bool,
7740
+ uncovered : f32,
7741
+ count : u32,
7742
+ supportCount : u32,
7743
+ centerColor : vec4<f32>,
7744
+ centerMoving : bool,
7745
+ ) -> vec4<f32> {
7746
+ // Uncovered ownership is decremented at the same time as validCount is incremented
7747
+ // for both geometric and midpoint-recovered samples. Keep the derived cap
7748
+ // as a guard against floating point accumulation and make the ownership
7749
+ // transfer explicit instead of adding an independent brightness term.
7750
+ let conservedUncovered = min(
7751
+ max(uncovered, 0.0),
7752
+ max(f32(supportCount) - validCount, 0.0),
7753
+ );
7754
+ // A moving receiver can be partially covered at a silhouette edge. Use the
7755
+ // accepted foreground only for the receiver's missing current coverage; a
7756
+ // fully covered receiver keeps the original center contribution and the
7757
+ // Dawn energy contract remains unchanged for uniform opaque surfaces.
7758
+ let acceptedColor = select(
7759
+ vec3<f32>(0.0),
7760
+ filtered.rgb / max(validCount, 1.0),
7761
+ hasAcceptedSource && validCount > 0.0,
7762
+ );
7763
+ // Preserve the color target's authored alpha as the output floor. It is
7764
+ // separate from the RGB test below, which identifies an empty background
7765
+ // receiver without treating alpha as a scene-coverage attachment.
7766
+ let missingForeground = 1.0 - clamp(centerColor.a, 0.0, 1.0);
7767
+ let edgeCoverage = select(missingForeground, 1.0, centerMoving);
7768
+ let edgeFill = acceptedColor * edgeCoverage * EDGE_FILL_FACTOR;
7769
+ let movingFallback = select(centerColor.rgb, edgeFill, hasAcceptedSource && validCount > 0.0);
7770
+ let background = select(vec3<f32>(0.0), movingFallback, centerMoving);
7771
+ // The accepted colour can come from a transparent receiver's moving
7772
+ // foreground. Carry the same bounded support coverage into alpha so that
7773
+ // RGB written outside the current silhouette is not composited as black.
7774
+ // Preserve the receiver's authored coverage as a floor: blur must not make
7775
+ // an already visible pixel disappear.
7776
+ let acceptedAlpha = select(
7777
+ 0.0,
7778
+ filtered.a / max(validCount, 1.0),
7779
+ validCount > 0.0,
7780
+ );
7781
+ let acceptedCoverage = validCount + missingForeground * conservedUncovered;
7782
+ let reconstructedAlpha = acceptedAlpha * acceptedCoverage / max(f32(count), 1.0);
7783
+ let outputAlpha = clamp(max(centerColor.a, reconstructedAlpha), 0.0, 1.0);
7784
+ let emptyReceiver =
7785
+ !centerMoving &&
7786
+ hasAcceptedSource &&
7787
+ max(max(abs(centerColor.r), abs(centerColor.g)), abs(centerColor.b)) < 1e-4;
7788
+ let sourceScale = select(1.0, 1.0 - EDGE_FILL_FACTOR, emptyReceiver);
7789
+ let sourceContribution = filtered.rgb * sourceScale;
7790
+ let trailScale = select(1.0, 1.0 - EDGE_FILL_FACTOR, emptyReceiver);
7791
+ let trailContribution = background * conservedUncovered * trailScale;
7792
+ return vec4<f32>(
7793
+ (sourceContribution + centerColor.rgb + trailContribution) / max(f32(count), 1.0),
7794
+ outputAlpha,
7795
+ );
7796
+ }
7797
+
7798
+ fn longSupportFilter(
7799
+ receiverPixel : vec2<i32>,
7800
+ support : MotionBlurSupport,
7801
+ centerDepth : f32,
7802
+ dimensions : vec2<i32>,
7803
+ ) -> MotionBlurFilter {
7804
+ var empty = MotionBlurFilter(
7805
+ vec4<f32>(0.0),
7806
+ vec4<f32>(0.0),
7807
+ vec4<f32>(0.0),
7808
+ vec4<f32>(0.0),
7809
+ 0.0,
7810
+ 0.0,
7811
+ 0.0,
7812
+ 0.0,
7813
+ 0.0,
7814
+ 0.0,
7815
+ 0.0,
7816
+ 0.0,
7817
+ receiverPixel,
7818
+ vec2<f32>(0.0),
7819
+ 0.0,
7820
+ 0u,
7821
+ 0u,
7822
+ false,
7823
+ );
7824
+ if (!support.valid) { return empty; }
7825
+ let count = min(params.sampleCount, 16u);
7826
+ // The authored tier is the total source colour-read budget. Reserve one slot
7827
+ // for the receiver/background contribution; the remaining bounded taps
7828
+ // (at most 15) are split between the two source directions when needed.
7829
+ let supportCount = max(count - 1u, 1u);
7830
+ let useSecondDirection = support.secondaryRadius > 1e-5 && distinctDirection(support.primaryMotion, support.secondaryMotion);
7831
+ let primaryCount = select(supportCount, max(supportCount / 2u, 1u), useSecondDirection);
7832
+ let secondaryCount = max(supportCount - primaryCount, 1u);
7833
+ let shutter = clamp(params.shutterAngle / 360.0, 0.0, 1.0);
7834
+ let center = temporalAt(receiverPixel, dimensions);
7835
+ // Keep each receiving quadrant's own center color. Reusing the owner
7836
+ // pixel for all four quadrants causes a one-pixel parity shift to duplicate
7837
+ // a bright source (or erase a bright receiver) in the half-resolution cache.
7838
+ var centerColor0 = colorAt(receiverPixel, dimensions);
7839
+ var centerColor1 = colorAt(receiverPixel + vec2<i32>(1, 0), dimensions);
7840
+ var centerColor2 = colorAt(receiverPixel + vec2<i32>(0, 1), dimensions);
7841
+ var centerColor3 = colorAt(receiverPixel + vec2<i32>(1, 1), dimensions);
7842
+ var filtered0 = vec4<f32>(0.0);
7843
+ var filtered1 = vec4<f32>(0.0);
7844
+ var filtered2 = vec4<f32>(0.0);
7845
+ var filtered3 = vec4<f32>(0.0);
7846
+ var validCount0 = 0.0;
7847
+ var validCount1 = 0.0;
7848
+ var validCount2 = 0.0;
7849
+ var validCount3 = 0.0;
7850
+ var hasAcceptedSource0 = false;
7851
+ var hasAcceptedSource1 = false;
7852
+ var hasAcceptedSource2 = false;
7853
+ var hasAcceptedSource3 = false;
7854
+ var uncovered0 = f32(supportCount);
7855
+ var uncovered1 = f32(supportCount);
7856
+ var uncovered2 = f32(supportCount);
7857
+ var uncovered3 = f32(supportCount);
7858
+ var midpointFallbackPixel = vec2<i32>(0);
7859
+ var midpointFallbackValid = false;
7860
+ var midpointFallbackRadius = 0.0;
7861
+ var nearestDepth0 = 3.402823e+38;
7862
+ var nearestDepth1 = 3.402823e+38;
7863
+ var nearestDepth2 = 3.402823e+38;
7864
+ var nearestDepth3 = 3.402823e+38;
7865
+ for (var index = 0u; index < 16u; index += 1u) {
7866
+ if (index >= supportCount) { break; }
7867
+ let secondDirection = useSecondDirection && index >= primaryCount;
7868
+ let localIndex = select(index, index - primaryCount, secondDirection);
7869
+ let localCount = select(primaryCount, secondaryCount, secondDirection);
7870
+ let selectedMotion = select(support.primaryMotion, support.secondaryMotion, secondDirection);
7871
+ let selectedRadius = select(support.primaryRadius, support.secondaryRadius, secondDirection);
7872
+ let tap = supportTap(
7873
+ receiverPixel,
7874
+ localIndex,
7875
+ localCount,
7876
+ selectedMotion,
7877
+ selectedRadius,
7878
+ select(support.primaryAnchor, support.secondaryAnchor, secondDirection),
7879
+ dimensions,
7880
+ );
7881
+ if (!tap.valid) {
7882
+ if (tap.fallbackValid && selectedRadius > midpointFallbackRadius) {
7883
+ midpointFallbackPixel = tap.fallbackPixel;
7884
+ midpointFallbackValid = true;
7885
+ midpointFallbackRadius = selectedRadius;
7886
+ }
7887
+ continue;
7888
+ }
7889
+ let source = sourceAtFootprint(
7890
+ receiverPixel,
7891
+ tap.pixel,
7892
+ centerDepth,
7893
+ shutter,
7894
+ dimensions,
7895
+ );
7896
+ if (!source.valid) {
7897
+ if (tap.fallbackValid && selectedRadius > midpointFallbackRadius) {
7898
+ midpointFallbackPixel = tap.fallbackPixel;
7899
+ midpointFallbackValid = true;
7900
+ midpointFallbackRadius = selectedRadius;
7901
+ }
7902
+ continue;
7903
+ }
7904
+ let sampleColor = colorAt(source.pixel, dimensions);
7905
+ // Keep the receiver's own color as the center contribution. A source
7906
+ // footprint may cover an adjacent receiver quadrant without being the
7907
+ // receiver itself; substituting its color here creates odd/even-alignment
7908
+ // brightening at thin-object boundaries.
7909
+ if ((source.receiverMask & 1u) != 0u) {
7910
+ filtered0 += sampleColor * source.coverage0;
7911
+ validCount0 += source.coverage0;
7912
+ hasAcceptedSource0 = true;
7913
+ uncovered0 = max(uncovered0 - source.coverage0, 0.0);
7914
+ nearestDepth0 = min(nearestDepth0, source.depth);
7915
+ }
7916
+ if ((source.receiverMask & 2u) != 0u) {
7917
+ filtered1 += sampleColor * source.coverage1;
7918
+ validCount1 += source.coverage1;
7919
+ hasAcceptedSource1 = true;
7920
+ uncovered1 = max(uncovered1 - source.coverage1, 0.0);
7921
+ nearestDepth1 = min(nearestDepth1, source.depth);
7922
+ }
7923
+ if ((source.receiverMask & 4u) != 0u) {
7924
+ filtered2 += sampleColor * source.coverage2;
7925
+ validCount2 += source.coverage2;
7926
+ hasAcceptedSource2 = true;
7927
+ uncovered2 = max(uncovered2 - source.coverage2, 0.0);
7928
+ nearestDepth2 = min(nearestDepth2, source.depth);
7929
+ }
7930
+ if ((source.receiverMask & 8u) != 0u) {
7931
+ filtered3 += sampleColor * source.coverage3;
7932
+ validCount3 += source.coverage3;
7933
+ hasAcceptedSource3 = true;
7934
+ uncovered3 = max(uncovered3 - source.coverage3, 0.0);
7935
+ nearestDepth3 = min(nearestDepth3, source.depth);
7936
+ }
7937
+ }
7938
+ // A tile anchor recovers a sparse source only for the widest support tier,
7939
+ // where the fixed geometric lattice can leave a source between taps. It
7940
+ // spends the represented midpoint ownership of the finite support segment;
7941
+ // every accepted colour, coverage and depth contribution uses that same
7942
+ // bounded ownership, so the fallback cannot create an opaque stripe. The
7943
+ // shorter tiers keep their exact geometric energy and do not need recovery.
7944
+ if (midpointFallbackValid && max(support.primaryRadius, support.secondaryRadius) > 32.0) {
7945
+ let fallbackSource = sourceAtFootprint(
7946
+ receiverPixel,
7947
+ midpointFallbackPixel,
7948
+ centerDepth,
7949
+ shutter,
7950
+ dimensions,
7951
+ );
7952
+ if (fallbackSource.valid) {
7953
+ let midpointWeight = midpointTileOwnership(receiverPixel, fallbackSource);
7954
+ if (
7955
+ midpointWeight > 0.0
7956
+ ) {
7957
+ let fallbackColor = colorAt(fallbackSource.pixel, dimensions);
7958
+ let fallbackWeight0 = fallbackSource.coverage0 * midpointWeight;
7959
+ let fallbackWeight1 = fallbackSource.coverage1 * midpointWeight;
7960
+ let fallbackWeight2 = fallbackSource.coverage2 * midpointWeight;
7961
+ let fallbackWeight3 = fallbackSource.coverage3 * midpointWeight;
7962
+ if (validCount0 <= 0.0 && (fallbackSource.receiverMask & 1u) != 0u) {
7963
+ let ownedWeight0 = min(fallbackWeight0, uncovered0);
7964
+ if (ownedWeight0 > 0.0) {
7965
+ filtered0 += fallbackColor * ownedWeight0;
7966
+ validCount0 += ownedWeight0;
7967
+ hasAcceptedSource0 = true;
7968
+ uncovered0 = max(uncovered0 - ownedWeight0, 0.0);
7969
+ nearestDepth0 = min(nearestDepth0, fallbackSource.depth);
7970
+ }
7971
+ }
7972
+ if (validCount1 <= 0.0 && (fallbackSource.receiverMask & 2u) != 0u) {
7973
+ let ownedWeight1 = min(fallbackWeight1, uncovered1);
7974
+ if (ownedWeight1 > 0.0) {
7975
+ filtered1 += fallbackColor * ownedWeight1;
7976
+ validCount1 += ownedWeight1;
7977
+ hasAcceptedSource1 = true;
7978
+ uncovered1 = max(uncovered1 - ownedWeight1, 0.0);
7979
+ nearestDepth1 = min(nearestDepth1, fallbackSource.depth);
7980
+ }
7981
+ }
7982
+ if (validCount2 <= 0.0 && (fallbackSource.receiverMask & 4u) != 0u) {
7983
+ let ownedWeight2 = min(fallbackWeight2, uncovered2);
7984
+ if (ownedWeight2 > 0.0) {
7985
+ filtered2 += fallbackColor * ownedWeight2;
7986
+ validCount2 += ownedWeight2;
7987
+ hasAcceptedSource2 = true;
7988
+ uncovered2 = max(uncovered2 - ownedWeight2, 0.0);
7989
+ nearestDepth2 = min(nearestDepth2, fallbackSource.depth);
7990
+ }
7991
+ }
7992
+ if (validCount3 <= 0.0 && (fallbackSource.receiverMask & 8u) != 0u) {
7993
+ let ownedWeight3 = min(fallbackWeight3, uncovered3);
7994
+ if (ownedWeight3 > 0.0) {
7995
+ filtered3 += fallbackColor * ownedWeight3;
7996
+ validCount3 += ownedWeight3;
7997
+ hasAcceptedSource3 = true;
7998
+ uncovered3 = max(uncovered3 - ownedWeight3, 0.0);
7999
+ nearestDepth3 = min(nearestDepth3, fallbackSource.depth);
8000
+ }
8001
+ }
8002
+ }
8003
+ }
8004
+ }
8005
+ let moving0 = temporalAt(receiverPixel, dimensions);
8006
+ let moving1 = temporalAt(receiverPixel + vec2<i32>(1, 0), dimensions);
8007
+ let moving2 = temporalAt(receiverPixel + vec2<i32>(0, 1), dimensions);
8008
+ let moving3 = temporalAt(receiverPixel + vec2<i32>(1, 1), dimensions);
8009
+ let motion0 = sourceMotionPixels(moving0, dimensions);
8010
+ let motion1 = sourceMotionPixels(moving1, dimensions);
8011
+ let motion2 = sourceMotionPixels(moving2, dimensions);
8012
+ let motion3 = sourceMotionPixels(moving3, dimensions);
8013
+ let centerMoving0 = moving0.validDepth && moving0.motionValid && sourceRadius(motion0, shutter) > 1e-5;
8014
+ let centerMoving1 = moving1.validDepth && moving1.motionValid && sourceRadius(motion1, shutter) > 1e-5;
8015
+ let centerMoving2 = moving2.validDepth && moving2.motionValid && sourceRadius(motion2, shutter) > 1e-5;
8016
+ let centerMoving3 = moving3.validDepth && moving3.motionValid && sourceRadius(motion3, shutter) > 1e-5;
8017
+ empty.color0 = integrateLongColor(filtered0, validCount0, hasAcceptedSource0, uncovered0, count, supportCount, centerColor0, centerMoving0);
8018
+ empty.color1 = integrateLongColor(filtered1, validCount1, hasAcceptedSource1, uncovered1, count, supportCount, centerColor1, centerMoving1);
8019
+ empty.color2 = integrateLongColor(filtered2, validCount2, hasAcceptedSource2, uncovered2, count, supportCount, centerColor2, centerMoving2);
8020
+ empty.color3 = integrateLongColor(filtered3, validCount3, hasAcceptedSource3, uncovered3, count, supportCount, centerColor3, centerMoving3);
8021
+ empty.depth0 = select(centerDepth, nearestDepth0, validCount0 > 0.0);
8022
+ empty.depth1 = select(centerDepth, nearestDepth1, validCount1 > 0.0);
8023
+ empty.depth2 = select(centerDepth, nearestDepth2, validCount2 > 0.0);
8024
+ empty.depth3 = select(centerDepth, nearestDepth3, validCount3 > 0.0);
8025
+ empty.coverage0 = validCount0 / max(f32(supportCount), 1.0);
8026
+ empty.coverage1 = validCount1 / max(f32(supportCount), 1.0);
8027
+ empty.coverage2 = validCount2 / max(f32(supportCount), 1.0);
8028
+ empty.coverage3 = validCount3 / max(f32(supportCount), 1.0);
8029
+ empty.motion = select(support.primaryMotion, support.secondaryMotion, support.primaryRadius <= 1e-5);
8030
+ empty.radius = max(support.primaryRadius, support.secondaryRadius);
8031
+ empty.receiverMask =
8032
+ select(0u, 1u, validCount0 > 0.0 || centerMoving0) |
8033
+ select(0u, 2u, validCount1 > 0.0 || centerMoving1) |
8034
+ select(0u, 4u, validCount2 > 0.0 || centerMoving2) |
8035
+ select(0u, 8u, validCount3 > 0.0 || centerMoving3);
8036
+ empty.validMask =
8037
+ select(0u, 1u, validCount0 > 0.0 || centerMoving0) |
8038
+ select(0u, 2u, validCount1 > 0.0 || centerMoving1) |
8039
+ select(0u, 4u, validCount2 > 0.0 || centerMoving2) |
8040
+ select(0u, 8u, validCount3 > 0.0 || centerMoving3);
8041
+ empty.valid = empty.validMask != 0u;
8042
+ return empty;
8043
+ }
8044
+
8045
+ fn colorAt(pixel : vec2<i32>, dimensions : vec2<i32>) -> vec4<f32> {
8046
+ return textureLoad(currentColor, clamp(pixel, vec2<i32>(0), dimensions - vec2<i32>(1)), 0);
8047
+ }
8048
+
8049
+ fn cacheFootprintContains(receiverPixel : vec2<i32>, ownerPixel : vec2<i32>) -> bool {
8050
+ return
8051
+ receiverPixel.x >= ownerPixel.x && receiverPixel.x <= ownerPixel.x + 1 &&
8052
+ receiverPixel.y >= ownerPixel.y && receiverPixel.y <= ownerPixel.y + 1;
8053
+ }
8054
+
8055
+ fn cachedSupportCovers(
8056
+ receiverPixel : vec2<i32>,
8057
+ ownerPixel : vec2<i32>,
8058
+ motion : vec2<f32>,
8059
+ radius : f32,
8060
+ ) -> bool {
8061
+ if (!cacheFootprintContains(receiverPixel, ownerPixel)) { return false; }
8062
+ let motionLength = length(motion);
8063
+ if (motionLength <= 1e-5 || radius <= 1e-5) { return false; }
8064
+ let direction = motion / motionLength;
8065
+ let delta = vec2<f32>(receiverPixel - ownerPixel);
8066
+ let along = dot(delta, direction);
8067
+ let perpendicular = abs(delta.x * direction.y - delta.y * direction.x);
8068
+ return abs(along) <= radius + 1.0 && perpendicular <= 1.0;
8069
+ }
8070
+
8071
+ fn cachedDirectionMatches(support : MotionBlurSupport, motion : vec2<f32>) -> bool {
8072
+ let motionLength = length(motion);
8073
+ if (motionLength <= 1e-5) { return false; }
8074
+ let direction = motion / motionLength;
8075
+ if (support.primaryRadius > 1e-5 && abs(dot(normalize(support.primaryMotion), direction)) >= 0.94) {
8076
+ return true;
8077
+ }
8078
+ return support.secondaryRadius > 1e-5 && abs(dot(normalize(support.secondaryMotion), direction)) >= 0.94;
8079
+ }
8080
+
8081
+ fn depthGuidedHalfResolutionColor(
8082
+ localId : vec3<u32>,
8083
+ receiverPixel : vec2<i32>,
8084
+ support : MotionBlurSupport,
8085
+ centerDepth : f32,
8086
+ ) -> MotionBlurCacheSample {
8087
+ let cell = vec2<i32>(localId.xy / vec2<u32>(2));
8088
+ let quadrant = (localId.y % 2u) * 2u + (localId.x % 2u);
8089
+ var selected = MotionBlurCacheSample(vec4<f32>(0.0), 0.0, false);
8090
+ var selectedDelta = 3.402823e+38;
8091
+ // Search the bounded adjacent cache cells, but accept only a cell whose
8092
+ // authored 2x2 footprint and direction actually cover this receiver.
8093
+ for (var y = -1; y <= 1; y += 1) {
8094
+ for (var x = -1; x <= 1; x += 1) {
8095
+ let candidateCell = cell + vec2<i32>(x, y);
8096
+ if (candidateCell.x < 0 || candidateCell.y < 0 || candidateCell.x >= 4 || candidateCell.y >= 4) { continue; }
8097
+ let cacheCellIndex = u32(candidateCell.y * 4 + candidateCell.x);
8098
+ let index = cacheCellIndex * 4u + quadrant;
8099
+ if (
8100
+ halfFilteredValid[index] == 0u ||
8101
+ depthReject(centerDepth, halfFilteredDepth[index]) ||
8102
+ !cachedDirectionMatches(support, halfFilteredMotion[cacheCellIndex]) ||
8103
+ !cachedSupportCovers(
8104
+ receiverPixel,
8105
+ halfFilteredReceiver[cacheCellIndex],
8106
+ halfFilteredMotion[cacheCellIndex],
8107
+ halfFilteredRadius[cacheCellIndex],
8108
+ )
8109
+ ) { continue; }
8110
+ let delta = abs(halfFilteredDepth[index] - centerDepth);
8111
+ if (!selected.valid || delta < selectedDelta) {
8112
+ selected.color = halfFilteredColor[index];
8113
+ selected.coverage = halfFilteredCoverage[index];
8114
+ selectedDelta = delta;
8115
+ selected.valid = true;
8116
+ }
8117
+ }
8118
+ }
8119
+ return selected;
8120
+ }
8121
+
8122
+ @compute @workgroup_size(8, 8)
8123
+ fn tile_summary(
8124
+ @builtin(global_invocation_id) id : vec3<u32>,
8125
+ @builtin(local_invocation_id) localId : vec3<u32>,
8126
+ @builtin(local_invocation_index) localIndex : u32,
8127
+ @builtin(workgroup_id) workgroupId : vec3<u32>,
8128
+ ) {
8129
+ let dimensions = textureDimensions(sceneTemporal);
8130
+ let tiles = (dimensions + vec2<u32>(15)) / vec2<u32>(16);
8131
+ let tile = workgroupId.xy;
8132
+ let inTile = tile.x < tiles.x && tile.y < tiles.y;
8133
+ var maxMotion = vec2<f32>(0.0);
8134
+ var maxLength = 0.0;
8135
+ var maxSource = vec2<i32>(0);
8136
+ var secondaryMotion = vec2<f32>(0.0);
8137
+ var secondaryLength = 0.0;
8138
+ var secondarySource = vec2<i32>(0);
8139
+ for (var localY = 0u; localY < 2u; localY += 1u) {
8140
+ for (var localX = 0u; localX < 2u; localX += 1u) {
8141
+ let pixel = tile * vec2<u32>(16) + localId.xy * vec2<u32>(2) + vec2<u32>(localX, localY);
8142
+ if (!inTile || pixel.x >= dimensions.x || pixel.y >= dimensions.y) { continue; }
8143
+ let sample = temporalAt(vec2<i32>(pixel), vec2<i32>(dimensions));
8144
+ if (!sample.validDepth || !sample.motionValid) { continue; }
8145
+ let motionLength = length(sample.motion * vec2<f32>(dimensions));
8146
+ if (motionLength > maxLength) {
8147
+ if (distinctDirection(maxMotion, sample.motion)) {
8148
+ secondaryMotion = maxMotion;
8149
+ secondaryLength = maxLength;
8150
+ secondarySource = maxSource;
8151
+ }
8152
+ maxLength = motionLength;
8153
+ maxMotion = sample.motion;
8154
+ maxSource = vec2<i32>(pixel);
8155
+ } else if (
8156
+ motionLength > secondaryLength &&
8157
+ distinctDirection(maxMotion, sample.motion)
8158
+ ) {
8159
+ secondaryLength = motionLength;
8160
+ secondaryMotion = sample.motion;
8161
+ secondarySource = vec2<i32>(pixel);
8162
+ }
8163
+ }
8164
+ }
8165
+ tileMotionScratch[localIndex] = maxMotion;
8166
+ tileLengthScratch[localIndex] = maxLength;
8167
+ tileSourceScratch[localIndex] = maxSource;
8168
+ tileSecondaryMotionScratch[localIndex] = secondaryMotion;
8169
+ tileSecondaryLengthScratch[localIndex] = secondaryLength;
8170
+ tileSecondarySourceScratch[localIndex] = secondarySource;
8171
+ workgroupBarrier();
8172
+ for (var stride = 32u; stride > 0u; stride = stride / 2u) {
8173
+ if (localIndex < stride) {
8174
+ let otherIndex = localIndex + stride;
8175
+ let otherLength = tileLengthScratch[otherIndex];
8176
+ if (otherLength > tileLengthScratch[localIndex]) {
8177
+ let oldMotion = tileMotionScratch[localIndex];
8178
+ let oldLength = tileLengthScratch[localIndex];
8179
+ let oldSource = tileSourceScratch[localIndex];
8180
+ tileLengthScratch[localIndex] = otherLength;
8181
+ tileMotionScratch[localIndex] = tileMotionScratch[otherIndex];
8182
+ tileSourceScratch[localIndex] = tileSourceScratch[otherIndex];
8183
+ if (
8184
+ oldLength > 1e-5 &&
8185
+ distinctDirection(tileMotionScratch[localIndex], oldMotion) &&
8186
+ oldLength > tileSecondaryLengthScratch[localIndex]
8187
+ ) {
8188
+ tileSecondaryLengthScratch[localIndex] = oldLength;
8189
+ tileSecondaryMotionScratch[localIndex] = oldMotion;
8190
+ tileSecondarySourceScratch[localIndex] = oldSource;
8191
+ }
8192
+ }
8193
+ if (
8194
+ tileSecondaryLengthScratch[otherIndex] > tileSecondaryLengthScratch[localIndex] &&
8195
+ distinctDirection(
8196
+ tileMotionScratch[localIndex],
8197
+ tileSecondaryMotionScratch[otherIndex],
8198
+ )
8199
+ ) {
8200
+ tileSecondaryLengthScratch[localIndex] = tileSecondaryLengthScratch[otherIndex];
8201
+ tileSecondaryMotionScratch[localIndex] = tileSecondaryMotionScratch[otherIndex];
8202
+ tileSecondarySourceScratch[localIndex] = tileSecondarySourceScratch[otherIndex];
8203
+ }
8204
+ if (
8205
+ tileLengthScratch[otherIndex] > tileSecondaryLengthScratch[localIndex] &&
8206
+ distinctDirection(tileMotionScratch[localIndex], tileMotionScratch[otherIndex])
8207
+ ) {
8208
+ tileSecondaryLengthScratch[localIndex] = tileLengthScratch[otherIndex];
8209
+ tileSecondaryMotionScratch[localIndex] = tileMotionScratch[otherIndex];
8210
+ tileSecondarySourceScratch[localIndex] = tileSourceScratch[otherIndex];
8211
+ }
8212
+ }
8213
+ workgroupBarrier();
8214
+ }
8215
+ if (localIndex == 0u && inTile) {
8216
+ let tileIndex = tile.y * tiles.x + tile.x;
8217
+ tileSummary[tileIndex] = MotionBlurTileSummary(
8218
+ vec4<f32>(
8219
+ tileMotionScratch[0],
8220
+ f32(tileSourceScratch[0].x),
8221
+ f32(tileSourceScratch[0].y),
8222
+ ),
8223
+ vec4<f32>(
8224
+ tileSecondaryMotionScratch[0],
8225
+ f32(tileSecondarySourceScratch[0].x),
8226
+ f32(tileSecondarySourceScratch[0].y),
8227
+ ),
8228
+ );
8229
+ }
8230
+ }
8231
+
8232
+ @compute @workgroup_size(8, 8)
8233
+ fn reconstruct(
8234
+ @builtin(global_invocation_id) id : vec3<u32>,
8235
+ @builtin(local_invocation_id) localId : vec3<u32>,
8236
+ @builtin(workgroup_id) workgroupId : vec3<u32>,
8237
+ ) {
8238
+ let dimensions = textureDimensions(currentColor);
8239
+ let colorDimensions = vec2<i32>(dimensions);
8240
+ let pixel = vec2<i32>(id.xy);
8241
+ let inBounds = id.x < dimensions.x && id.y < dimensions.y;
8242
+ let workgroupBase = vec2<i32>(workgroupId.xy * vec2<u32>(8));
8243
+ let ownsHalfCell = (localId.x % 2u == 0u) && (localId.y % 2u == 0u);
8244
+ let halfIndex = (localId.y / 2u) * 4u + (localId.x / 2u);
8245
+ let localIndex = localId.y * 8u + localId.x;
8246
+ if (localIndex == 0u) {
8247
+ scanTileNeighbourhood(workgroupBase, colorDimensions);
8248
+ }
8249
+ workgroupBarrier();
8250
+ let support = resolveMotionBlurSupport(pixel, colorDimensions);
8251
+ let longVector = support.valid && support.longVector;
8252
+
8253
+ longVectorScratch[localId.y * 8u + localId.x] = select(0u, 1u, longVector);
8254
+ workgroupBarrier();
8255
+ for (var longStride = 32u; longStride > 0u; longStride = longStride / 2u) {
8256
+ let localIndex = localId.y * 8u + localId.x;
8257
+ if (localIndex < longStride) {
8258
+ longVectorScratch[localIndex] = max(
8259
+ longVectorScratch[localIndex],
8260
+ longVectorScratch[localIndex + longStride],
8261
+ );
8262
+ }
8263
+ workgroupBarrier();
8264
+ }
8265
+ let anyLongVector = longVectorScratch[0] != 0u;
8266
+ if (anyLongVector && ownsHalfCell) {
8267
+ // Every designated owner evaluates the full support exactly once. The
8268
+ // cached result includes filtered color, nearest accepted depth, and
8269
+ // coverage; no 2x2 prefilter is substituted for the long gather.
8270
+ let sourcePixel = workgroupBase + vec2<i32>(localId.xy);
8271
+ let sourceSupport = resolveMotionBlurSupport(sourcePixel, colorDimensions);
8272
+ let filtered = longSupportFilter(
8273
+ sourcePixel,
8274
+ sourceSupport,
8275
+ temporalAt(sourcePixel, colorDimensions).depth,
8276
+ colorDimensions,
8277
+ );
8278
+ halfFilteredReceiver[halfIndex] = sourcePixel;
8279
+ halfFilteredMotion[halfIndex] = filtered.motion;
8280
+ halfFilteredRadius[halfIndex] = filtered.radius;
8281
+ let cacheBase = halfIndex * 4u;
8282
+ halfFilteredColor[cacheBase] = filtered.color0;
8283
+ halfFilteredColor[cacheBase + 1u] = filtered.color1;
8284
+ halfFilteredColor[cacheBase + 2u] = filtered.color2;
8285
+ halfFilteredColor[cacheBase + 3u] = filtered.color3;
8286
+ halfFilteredDepth[cacheBase] = filtered.depth0;
8287
+ halfFilteredDepth[cacheBase + 1u] = filtered.depth1;
8288
+ halfFilteredDepth[cacheBase + 2u] = filtered.depth2;
8289
+ halfFilteredDepth[cacheBase + 3u] = filtered.depth3;
8290
+ halfFilteredCoverage[cacheBase] = filtered.coverage0;
8291
+ halfFilteredCoverage[cacheBase + 1u] = filtered.coverage1;
8292
+ halfFilteredCoverage[cacheBase + 2u] = filtered.coverage2;
8293
+ halfFilteredCoverage[cacheBase + 3u] = filtered.coverage3;
8294
+ halfFilteredValid[cacheBase] = select(0u, 1u, (filtered.validMask & 1u) != 0u);
8295
+ halfFilteredValid[cacheBase + 1u] = select(0u, 1u, (filtered.validMask & 2u) != 0u);
8296
+ halfFilteredValid[cacheBase + 2u] = select(0u, 1u, (filtered.validMask & 4u) != 0u);
8297
+ halfFilteredValid[cacheBase + 3u] = select(0u, 1u, (filtered.validMask & 8u) != 0u);
8298
+ }
8299
+ workgroupBarrier();
8300
+ // No invocation can return before both uniform barriers above. This keeps
8301
+ // odd extents and out-of-bounds lanes from allowing a long source to read a
8302
+ // cache entry that another invocation has not published yet.
8303
+ if (!inBounds) {
8304
+ return;
8305
+ }
8306
+ if (!support.valid) {
8307
+ textureStore(outputColor, pixel, colorAt(pixel, colorDimensions));
8308
+ return;
8309
+ }
8310
+ if (longVector) {
8311
+ let cached = depthGuidedHalfResolutionColor(
8312
+ localId,
8313
+ pixel,
8314
+ support,
8315
+ temporalAt(pixel, colorDimensions).depth,
8316
+ );
8317
+ if (cached.valid) {
8318
+ textureStore(outputColor, pixel, cached.color);
8319
+ } else {
8320
+ textureStore(outputColor, pixel, colorAt(pixel, colorDimensions));
8321
+ }
8322
+ return;
8323
+ }
8324
+ let useSecondDirection = support.secondaryRadius > 1e-5 && distinctDirection(support.primaryMotion, support.secondaryMotion);
8325
+ let count = min(params.sampleCount, 16u);
8326
+ let supportCount = max(count - 1u, 1u);
8327
+ let primaryCount = select(supportCount, max(supportCount / 2u, 1u), useSecondDirection);
8328
+ let secondaryCount = max(supportCount - primaryCount, 1u);
8329
+ let shutter = clamp(params.shutterAngle / 360.0, 0.0, 1.0);
8330
+ let center = temporalAt(pixel, colorDimensions);
8331
+ let centerMotion = sourceMotionPixels(center, colorDimensions);
8332
+ let centerMoving = center.validDepth && center.motionValid && sourceRadius(centerMotion, shutter) > 1e-5;
8333
+ let centerColor = colorAt(pixel, colorDimensions);
8334
+ var accum = vec4<f32>(0.0);
8335
+ var weight = 0.0;
8336
+ for (var index = 0u; index < 16u; index += 1u) {
8337
+ if (index >= supportCount) { break; }
8338
+ let secondDirection = useSecondDirection && index >= primaryCount;
8339
+ let localIndex = select(index, index - primaryCount, secondDirection);
8340
+ let localCount = select(primaryCount, secondaryCount, secondDirection);
8341
+ let selectedMotion = select(support.primaryMotion, support.secondaryMotion, secondDirection);
8342
+ let selectedRadius = select(support.primaryRadius, support.secondaryRadius, secondDirection);
8343
+ let tap = supportTap(
8344
+ pixel,
8345
+ localIndex,
8346
+ localCount,
8347
+ selectedMotion,
8348
+ selectedRadius,
8349
+ select(support.primaryAnchor, support.secondaryAnchor, secondDirection),
8350
+ colorDimensions,
8351
+ );
8352
+ if (!tap.valid) { continue; }
8353
+ let source = sourceAt(pixel, tap.pixel, center.depth, shutter, colorDimensions);
8354
+ if (!source.valid) { continue; }
8355
+ let sampleColor = colorAt(source.pixel, colorDimensions);
8356
+ accum += sampleColor;
8357
+ weight += 1.0;
8358
+ }
8359
+ let uncovered = max(f32(supportCount) - weight, 0.0);
8360
+ let acceptedColor = select(vec3<f32>(0.0), accum.rgb / max(weight, 1.0), weight > 0.0);
8361
+ let missingForeground = 1.0 - clamp(centerColor.a, 0.0, 1.0);
8362
+ let edgeCoverage = select(missingForeground, 1.0, centerMoving);
8363
+ let edgeFill = acceptedColor * edgeCoverage * EDGE_FILL_FACTOR;
8364
+ let movingFallback = select(centerColor.rgb, edgeFill, weight > 0.0);
8365
+ let uncoveredColor = select(vec3<f32>(0.0), movingFallback, centerMoving);
8366
+ let acceptedAlpha = select(0.0, accum.a / max(weight, 1.0), weight > 0.0);
8367
+ let acceptedCoverage = weight + missingForeground * uncovered;
8368
+ let reconstructedAlpha = acceptedAlpha * acceptedCoverage / max(f32(count), 1.0);
8369
+ let outputAlpha = clamp(max(centerColor.a, reconstructedAlpha), 0.0, 1.0);
8370
+ let emptyReceiver =
8371
+ !centerMoving &&
8372
+ weight > 0.0 &&
8373
+ max(max(abs(centerColor.r), abs(centerColor.g)), abs(centerColor.b)) < 1e-4;
8374
+ let sourceScale = select(1.0, 1.0 - EDGE_FILL_FACTOR, emptyReceiver);
8375
+ let sourceContribution = accum.rgb * sourceScale;
8376
+ let trailScale = select(1.0, 1.0 - EDGE_FILL_FACTOR, emptyReceiver);
8377
+ let trailContribution = uncoveredColor * uncovered * trailScale;
8378
+ textureStore(
8379
+ outputColor,
8380
+ pixel,
8381
+ vec4<f32>((sourceContribution + centerColor.rgb + trailContribution) / max(f32(count), 1.0), outputAlpha),
8382
+ );
8383
+ }
8384
+ `;
8385
+ function encodeMotionBlurComputeParams(params, frameDeltaSeconds = 1 / 60, reset = false) {
8386
+ const bytes2 = new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE);
8387
+ const view = new DataView(bytes2.buffer);
8388
+ const uniformDeltaSeconds = Number.isFinite(frameDeltaSeconds) ? frameDeltaSeconds : 0;
8389
+ view.setFloat32(0, params.shutterAngle, true);
8390
+ view.setFloat32(4, params.maxRadiusPixels, true);
8391
+ view.setUint32(8, effectiveMotionBlurSampleCount(params.sampleCount), true);
8392
+ view.setUint32(12, reset ? 1 : 0, true);
8393
+ view.setFloat32(16, params.targetFps, true);
8394
+ view.setFloat32(20, motionBlurExposureScale(uniformDeltaSeconds, params.targetFps), true);
8395
+ view.setFloat32(24, uniformDeltaSeconds, true);
8396
+ view.setUint32(28, 0, true);
8397
+ return bytes2;
8398
+ }
8399
+ function positiveDimension(value) {
8400
+ return Number.isFinite(value) && value > 0 ? Math.ceil(value) : 1;
8401
+ }
8402
+ function planMotionBlur(params, context, temporal = {
8403
+ frameDeltaSeconds: 1 / 60,
8404
+ reset: false
8405
+ }) {
8406
+ if (!motionBlurTemporalDemand(params)) return ok$1({ resources: [], passes: [] });
8407
+ try {
8408
+ const sceneTemporal = context.sceneData.require(SCENE_DATA_TEMPORAL_V1_SCHEMA);
8409
+ const outputFormat = context.targets.find((target5) => target5.name === "motion-output")?.format ?? "rgba16float";
8410
+ const fullscreen = {
8411
+ kind: "fullscreen-program",
8412
+ name: "motion-blur",
8413
+ source: "forgeax::shader::motion-blur",
8414
+ reads: ["scene-color", "scene-temporal"],
8415
+ params: {
8416
+ byteSize: MOTION_BLUR_PARAMS_BYTE_SIZE,
8417
+ defaultValue: new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE)
8418
+ }
8419
+ };
8420
+ const computeCapable = context.caps.compute === true && context.caps.storageBuffer === true && context.caps.storageTexture === true && context.caps.rgba16floatRenderable === true && outputFormat === "rgba16float";
8421
+ if (computeCapable) {
8422
+ const width = positiveDimension(context.frame.width);
8423
+ const height = positiveDimension(context.frame.height);
8424
+ const tileWidth = Math.max(1, Math.ceil(width / 16));
8425
+ const tileHeight = Math.max(1, Math.ceil(height / 16));
8426
+ return ok$1({
8427
+ resources: [
8428
+ {
8429
+ kind: "compute-program",
8430
+ name: "motion-blur-compute",
8431
+ program: {
8432
+ wgsl: MOTION_BLUR_COMPUTE_WGSL,
8433
+ entryPoints: ["tile_summary", "reconstruct"],
8434
+ bindings: [
8435
+ {
8436
+ entries: [
8437
+ {
8438
+ binding: 0,
8439
+ visibility: 4,
8440
+ texture: { sampleType: "float", viewDimension: "2d" }
8441
+ },
8442
+ {
8443
+ binding: 1,
8444
+ visibility: 4,
8445
+ texture: { sampleType: "float", viewDimension: "2d" }
8446
+ },
8447
+ { binding: 2, visibility: 4, buffer: { type: "storage" } },
8448
+ {
8449
+ binding: 3,
8450
+ visibility: 4,
8451
+ storageTexture: {
8452
+ access: "write-only",
8453
+ format: "rgba16float",
8454
+ viewDimension: "2d"
8455
+ }
8456
+ },
8457
+ { binding: 4, visibility: 4, buffer: { type: "uniform" } }
8458
+ ]
8459
+ }
8460
+ ]
8461
+ }
8462
+ },
8463
+ {
8464
+ kind: "buffer",
8465
+ name: "motion-blur-tile-summary",
8466
+ size: tileWidth * tileHeight * 32,
8467
+ usage: ["storage"]
8468
+ },
8469
+ {
8470
+ kind: "buffer",
8471
+ name: "motion-blur-compute-params",
8472
+ size: MOTION_BLUR_PARAMS_BYTE_SIZE,
8473
+ usage: ["uniform"],
8474
+ data: encodeMotionBlurComputeParams(params, temporal.frameDeltaSeconds, temporal.reset)
8475
+ },
8476
+ {
8477
+ kind: "prepared-gpu-resource",
8478
+ name: "motion-blur-input",
8479
+ resource: { kind: "texture-view", value: void 0 },
8480
+ logicalTarget: "motion-input"
8481
+ },
8482
+ {
8483
+ kind: "prepared-gpu-resource",
8484
+ name: "motion-blur-temporal",
8485
+ resource: { kind: "texture-view", value: void 0 },
8486
+ logicalTarget: sceneTemporal
8487
+ },
8488
+ {
8489
+ kind: "prepared-gpu-resource",
8490
+ name: "motion-blur-output",
8491
+ resource: { kind: "texture-view", value: void 0 },
8492
+ logicalTarget: "motion-output"
8493
+ },
8494
+ {
8495
+ kind: "compute-bindings",
8496
+ name: "motion-blur-compute-bindings",
8497
+ program: "motion-blur-compute",
8498
+ entries: [
8499
+ { binding: 0, resource: "motion-blur-input" },
8500
+ { binding: 1, resource: "motion-blur-temporal" },
8501
+ { binding: 2, resource: "motion-blur-tile-summary" },
8502
+ { binding: 3, resource: "motion-blur-output" },
8503
+ { binding: 4, resource: "motion-blur-compute-params" }
8504
+ ]
8505
+ }
8506
+ ],
8507
+ passes: [
8508
+ {
8509
+ kind: "compute",
8510
+ name: "motion-blur-tile-summary",
8511
+ program: "motion-blur-compute",
8512
+ bindings: "motion-blur-compute-bindings",
8513
+ dispatches: [
8514
+ {
8515
+ kind: "direct",
8516
+ entryPoint: "tile_summary",
8517
+ workgroups: [tileWidth, tileHeight, 1]
8518
+ }
8519
+ ]
8520
+ },
8521
+ {
8522
+ kind: "compute",
8523
+ // Keep the public post-process identity stable while the compute
8524
+ // lane adds its private tile-summary producer before it.
8525
+ name: "motion-blur",
8526
+ program: "motion-blur-compute",
8527
+ bindings: "motion-blur-compute-bindings",
8528
+ dispatches: [
8529
+ {
8530
+ kind: "direct",
8531
+ entryPoint: "reconstruct",
8532
+ workgroups: [
8533
+ Math.max(1, Math.ceil(width / 8)),
8534
+ Math.max(1, Math.ceil(height / 8)),
8535
+ 1
8536
+ ]
8537
+ }
8538
+ ]
8539
+ }
8540
+ ]
8541
+ });
8542
+ }
8543
+ return ok$1({
8544
+ resources: [
8545
+ fullscreen,
8546
+ {
8547
+ kind: "graphics-program",
8548
+ name: "motion-blur-pipeline",
8549
+ program: {
8550
+ shader: MOTION_BLUR_FEATURE_IDENTITY,
8551
+ vertexLayout: "none",
8552
+ colorFormats: [outputFormat],
8553
+ sampleCount: 1
8554
+ }
8555
+ },
8556
+ {
8557
+ kind: "graphics-bindings",
8558
+ name: "motion-blur-bindings",
8559
+ program: "motion-blur-pipeline",
8560
+ values: {
8561
+ group: 1,
8562
+ fullscreen: true,
8563
+ shader: MOTION_BLUR_FEATURE_IDENTITY,
8564
+ input: "motion-input",
8565
+ temporal: sceneTemporal
8566
+ },
8567
+ logicalTargets: { input: "motion-input" }
8568
+ }
8569
+ ],
8570
+ passes: [
8571
+ {
8572
+ kind: "raster",
8573
+ name: "motion-blur",
8574
+ colorAttachments: [{ target: "motion-output", loadOp: "clear", storeOp: "store" }],
8575
+ sampledTargets: ["motion-input", sceneTemporal],
8576
+ draws: [
8577
+ {
8578
+ program: "motion-blur-pipeline",
8579
+ bindings: ["motion-blur-bindings"],
8580
+ vertexData: [],
8581
+ vertexLayout: "none",
8582
+ draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
8583
+ }
8584
+ ]
8585
+ }
8586
+ ]
8587
+ });
8588
+ } catch (cause) {
8589
+ return err$1(cause);
8590
+ }
8591
+ }
8592
+ function createMotionBlurFeature(read) {
8593
+ return Object.freeze({
8594
+ identity: MOTION_BLUR_FEATURE_IDENTITY,
8595
+ // The program is an Engine-owned static WGSL source. Immediate creation
8596
+ // lets a newly admitted frame use the same graph/dispatch receipt instead
8597
+ // of silently falling back to the raster post effect while the validated
8598
+ // manifest adapter is still warming.
8599
+ shaderModuleMode: "immediate",
8600
+ extract: (_context) => {
8601
+ const input = read();
8602
+ const validated = validateMotionBlurParams(input?.params);
8603
+ if (!validated.ok) return err$1(validated.error);
8604
+ const params = input?.params === void 0 ? void 0 : validated.value;
8605
+ return ok$1({
8606
+ params,
8607
+ demanded: motionBlurTemporalDemand(params),
8608
+ frameDeltaSeconds: input?.frameDeltaSeconds === void 0 ? 1 / 60 : input.frameDeltaSeconds,
8609
+ reset: input?.reset === true
8610
+ });
8611
+ },
8612
+ plan: (frame, context) => frame.params === void 0 ? ok$1({ resources: [], passes: [] }) : planMotionBlur(frame.params, context, frame)
8613
+ });
8614
+ }
6976
8615
  var AUTO_EXPOSURE_FUSED_GRAPH_PASS = "auto-exposure-meter";
6977
8616
  var AUTO_EXPOSURE_HISTOGRAM_DISPATCH = Object.freeze({
6978
8617
  x: 4,
@@ -7834,60 +9473,183 @@ function addStandardColorLutPass(graph, input, output, lut) {
7834
9473
  }
7835
9474
 
7836
9475
  // src/pipeline/standard-post.ts
9476
+ var STANDARD_BLOOM_MAX_LEVELS = 5;
7837
9477
  var STANDARD_BLOOM_TARGET_SPEC = {
7838
- composited: { label: "bloom-composited", format: "rgba16float", size: "surface" },
7839
- bright: { label: "bloom-bright", format: "rgba16float", size: "half-surface" },
7840
- blurH: { label: "bloom-blur-h", format: "rgba16float", size: "half-surface" },
7841
- blurV: { label: "bloom-blur-v", format: "rgba16float", size: "half-surface" }
9478
+ composited: { label: "bloom-composited", format: "rgba16float" },
9479
+ downsamplePrefix: "bloom-downsample-",
9480
+ upsamplePrefix: "bloom-upsample-"
7842
9481
  };
9482
+ function deriveStandardBloomLevels(outputWidth, outputHeight) {
9483
+ const levels = [];
9484
+ let width = Math.max(1, Math.ceil(outputWidth / 2));
9485
+ let height = Math.max(1, Math.ceil(outputHeight / 2));
9486
+ for (let level = 0; level < STANDARD_BLOOM_MAX_LEVELS; level += 1) {
9487
+ levels.push({ width, height });
9488
+ if (width === 1 && height === 1) break;
9489
+ width = Math.max(1, Math.ceil(width / 2));
9490
+ height = Math.max(1, Math.ceil(height / 2));
9491
+ }
9492
+ return levels;
9493
+ }
9494
+ function standardBloomTargetSpecs(outputWidth, outputHeight) {
9495
+ const levels = deriveStandardBloomLevels(outputWidth, outputHeight);
9496
+ return {
9497
+ composited: {
9498
+ label: STANDARD_BLOOM_TARGET_SPEC.composited.label,
9499
+ format: STANDARD_BLOOM_TARGET_SPEC.composited.format,
9500
+ size: { width: outputWidth, height: outputHeight },
9501
+ domain: "linear-hdr"
9502
+ },
9503
+ downsample: levels.map((size, level) => ({
9504
+ label: `${STANDARD_BLOOM_TARGET_SPEC.downsamplePrefix}${level}`,
9505
+ format: "rgba16float",
9506
+ size,
9507
+ domain: "linear-hdr"
9508
+ })),
9509
+ upsample: levels.slice(0, -1).map((size, level) => ({
9510
+ label: `${STANDARD_BLOOM_TARGET_SPEC.upsamplePrefix}${level}`,
9511
+ format: "rgba16float",
9512
+ size,
9513
+ domain: "linear-hdr"
9514
+ })),
9515
+ levels
9516
+ };
9517
+ }
7843
9518
  var emptyBloomGraphInspection = () => ({
7844
9519
  status: "empty",
7845
9520
  targetCount: 0,
7846
9521
  targetBytes: 0,
7847
- passCount: 0
9522
+ passCount: 0,
9523
+ levelCount: 0,
9524
+ levelDimensions: [],
9525
+ downsamplePassCount: 0,
9526
+ upsamplePassCount: 0
7848
9527
  });
7849
9528
  var invalidBloomGraphInspection = () => ({
7850
9529
  status: "invalid",
7851
9530
  targetCount: 0,
7852
9531
  targetBytes: 0,
7853
- passCount: 0
9532
+ passCount: 0,
9533
+ levelCount: 0,
9534
+ levelDimensions: [],
9535
+ downsamplePassCount: 0,
9536
+ upsamplePassCount: 0
7854
9537
  });
7855
- function textureBytes(resource, expected) {
9538
+ function textureBytes(resource) {
7856
9539
  const descriptor = resource.descriptor;
7857
- if (descriptor === void 0 || descriptor.kind !== "texture" || descriptor.format !== expected.format || !("size" in descriptor) || descriptor.size !== expected.size)
7858
- return 0;
9540
+ if (resource.kind !== "texture" || descriptor === void 0 || descriptor.kind !== "texture" || descriptor.format !== "rgba16float")
9541
+ return void 0;
7859
9542
  const texelBytes = 8;
7860
9543
  let bytes2 = 0;
7861
9544
  for (let level = 0; level < descriptor.mipLevelCount; level += 1) {
7862
9545
  bytes2 += Math.max(1, descriptor.width >> level) * Math.max(1, descriptor.height >> level) * descriptor.depthOrArrayLayers * texelBytes;
7863
9546
  }
7864
- return bytes2 * descriptor.sampleCount;
9547
+ const descriptorBytes = bytes2 * descriptor.sampleCount;
9548
+ return resource.byteSize === descriptorBytes && descriptorBytes > 0 ? descriptorBytes : void 0;
9549
+ }
9550
+ function resourceSize(resource) {
9551
+ const descriptor = resource.descriptor;
9552
+ if (resource.kind !== "texture" || descriptor.kind !== "texture") return void 0;
9553
+ if (typeof descriptor.size === "string") return void 0;
9554
+ if (resource.extent === void 0 || resource.extent.width !== descriptor.width || resource.extent.height !== descriptor.height || resource.extent.depthOrArrayLayers !== descriptor.depthOrArrayLayers) {
9555
+ return void 0;
9556
+ }
9557
+ if (descriptor.format !== "rgba16float" || descriptor.domain !== "linear-hdr" || descriptor.width <= 0 || descriptor.height <= 0 || descriptor.depthOrArrayLayers !== 1 || descriptor.mipLevelCount !== 1 || descriptor.sampleCount !== 1 || resource.dimension !== "2d" || descriptor.size.width !== descriptor.width || descriptor.size.height !== descriptor.height || textureBytes(resource) === void 0) {
9558
+ return void 0;
9559
+ }
9560
+ return { width: descriptor.width, height: descriptor.height };
9561
+ }
9562
+ function hasPassAccesses(pass, accesses) {
9563
+ return pass !== void 0 && pass.kind === "raster" && pass.accesses.length === accesses.length && accesses.every(
9564
+ (access, index) => pass.accesses[index]?.resource === access.resource && pass.accesses[index]?.usage === access.usage
9565
+ );
7865
9566
  }
7866
9567
  function inspectStandardBloomGraph(graph) {
7867
9568
  if (graph === void 0) return emptyBloomGraphInspection();
7868
- const targetSpec = new Map(
7869
- Object.values(STANDARD_BLOOM_TARGET_SPEC).map((entry) => [entry.label, entry])
9569
+ const composited = graph.resources.filter(
9570
+ (resource) => resource.label === STANDARD_BLOOM_TARGET_SPEC.composited.label
7870
9571
  );
7871
- const targets = graph.resources.flatMap((resource) => {
7872
- const expected = targetSpec.get(resource.label);
7873
- return expected === void 0 ? [] : [{ resource, expected }];
7874
- });
7875
- const passNames = new Set(Object.values(TYPED_BLOOM_PASS_NAMES));
7876
- const passes = graph.passes.filter((pass) => passNames.has(pass.name));
9572
+ const downsample = graph.resources.filter((resource) => resource.label.startsWith(STANDARD_BLOOM_TARGET_SPEC.downsamplePrefix)).sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true }));
9573
+ const upsample = graph.resources.filter((resource) => resource.label.startsWith(STANDARD_BLOOM_TARGET_SPEC.upsamplePrefix)).sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true }));
9574
+ const targets = [...composited, ...downsample, ...upsample];
9575
+ const passes = graph.passes.filter(
9576
+ (pass) => pass.name === TYPED_BLOOM_PASS_NAMES.composite || pass.name.startsWith(`${TYPED_BLOOM_PASS_NAMES.downsample}-`) || pass.name.startsWith(`${TYPED_BLOOM_PASS_NAMES.upsample}-`)
9577
+ ).sort((left, right) => left.executionIndex - right.executionIndex);
7877
9578
  if (targets.length === 0 && passes.length === 0) return emptyBloomGraphInspection();
7878
- const completeTargetSet = targets.length === Object.values(STANDARD_BLOOM_TARGET_SPEC).length && targets.every(({ resource, expected }) => textureBytes(resource, expected) > 0);
7879
- const completePassSet = passes.length === Object.values(TYPED_BLOOM_PASS_NAMES).length;
7880
- if (!completeTargetSet || !completePassSet) {
9579
+ if (composited.length !== 1) return invalidBloomGraphInspection();
9580
+ const compositedResource = composited[0];
9581
+ if (compositedResource === void 0) return invalidBloomGraphInspection();
9582
+ const compositedSize = resourceSize(compositedResource);
9583
+ if (compositedSize === void 0) return invalidBloomGraphInspection();
9584
+ const expected = standardBloomTargetSpecs(compositedSize.width, compositedSize.height);
9585
+ const expectedDownsampleLabels = expected.downsample.map((entry) => entry.label);
9586
+ const expectedUpsampleLabels = expected.upsample.map((entry) => entry.label);
9587
+ const orderedDownsample = downsample.sort(
9588
+ (left, right) => left.label.localeCompare(right.label, void 0, { numeric: true })
9589
+ );
9590
+ const orderedUpsample = upsample.sort(
9591
+ (left, right) => left.label.localeCompare(right.label, void 0, { numeric: true })
9592
+ );
9593
+ const completeTargetSet = targets.length === 1 + expectedDownsampleLabels.length + expectedUpsampleLabels.length && orderedDownsample.length === expectedDownsampleLabels.length && orderedUpsample.length === expectedUpsampleLabels.length && orderedDownsample.every(
9594
+ (resource, level) => resource.label === expectedDownsampleLabels[level]
9595
+ ) && orderedUpsample.every((resource, level) => resource.label === expectedUpsampleLabels[level]) && orderedDownsample.every(
9596
+ (resource, level) => resourceSize(resource)?.width === expected.levels[level]?.width && resourceSize(resource)?.height === expected.levels[level]?.height
9597
+ ) && orderedUpsample.every(
9598
+ (resource, level) => resourceSize(resource)?.width === expected.levels[level]?.width && resourceSize(resource)?.height === expected.levels[level]?.height
9599
+ );
9600
+ const expectedPassNames = [
9601
+ ...expectedDownsampleLabels,
9602
+ ...[...expectedUpsampleLabels].reverse(),
9603
+ TYPED_BLOOM_PASS_NAMES.composite
9604
+ ];
9605
+ const completePassSet = passes.length === expected.levels.length + expected.upsample.length + 1 && passes.every((pass, index) => pass.name === expectedPassNames[index]);
9606
+ const sceneLabel = passes[0]?.accesses[0]?.resource;
9607
+ const passTopology = completePassSet && sceneLabel !== void 0 && !sceneLabel.startsWith(STANDARD_BLOOM_TARGET_SPEC.downsamplePrefix) && !sceneLabel.startsWith(STANDARD_BLOOM_TARGET_SPEC.upsamplePrefix) && sceneLabel !== STANDARD_BLOOM_TARGET_SPEC.composited.label && graph.resources.some((resource) => resource.label === sceneLabel) && orderedDownsample.every(
9608
+ (_, level) => hasPassAccesses(passes[level], [
9609
+ {
9610
+ resource: level === 0 ? sceneLabel : `bloom-downsample-${level - 1}`,
9611
+ usage: "sampled-read"
9612
+ },
9613
+ { resource: `bloom-downsample-${level}`, usage: "color-attachment" }
9614
+ ])
9615
+ ) && orderedUpsample.every((_, level) => {
9616
+ const passIndex = expected.levels.length + expected.upsample.length - 1 - level;
9617
+ return hasPassAccesses(passes[passIndex], [
9618
+ { resource: `bloom-downsample-${level}`, usage: "sampled-read" },
9619
+ {
9620
+ resource: level === expected.levels.length - 2 ? `bloom-downsample-${level + 1}` : `bloom-upsample-${level + 1}`,
9621
+ usage: "sampled-read"
9622
+ },
9623
+ { resource: `bloom-upsample-${level}`, usage: "color-attachment" }
9624
+ ]);
9625
+ }) && hasPassAccesses(passes.at(-1), [
9626
+ { resource: sceneLabel, usage: "sampled-read" },
9627
+ {
9628
+ resource: expected.levels.length === 1 ? "bloom-downsample-0" : "bloom-upsample-0",
9629
+ usage: "sampled-read"
9630
+ },
9631
+ { resource: STANDARD_BLOOM_TARGET_SPEC.composited.label, usage: "color-attachment" }
9632
+ ]);
9633
+ if (!completeTargetSet || !passTopology) {
7881
9634
  return invalidBloomGraphInspection();
7882
9635
  }
9636
+ const levelDimensions = orderedDownsample.flatMap((resource) => {
9637
+ const size = resourceSize(resource);
9638
+ return size === void 0 ? [] : [size];
9639
+ });
9640
+ if (levelDimensions.length !== orderedDownsample.length) {
9641
+ return invalidBloomGraphInspection();
9642
+ }
9643
+ const targetBytes = targets.reduce((bytes2, resource) => bytes2 + (textureBytes(resource) ?? 0), 0);
7883
9644
  return {
7884
9645
  status: "valid",
7885
9646
  targetCount: targets.length,
7886
- targetBytes: targets.reduce(
7887
- (bytes2, target5) => bytes2 + textureBytes(target5.resource, target5.expected),
7888
- 0
7889
- ),
7890
- passCount: passes.length
9647
+ targetBytes,
9648
+ passCount: passes.length,
9649
+ levelCount: orderedDownsample.length,
9650
+ levelDimensions,
9651
+ downsamplePassCount: orderedDownsample.length,
9652
+ upsamplePassCount: orderedUpsample.length
7891
9653
  };
7892
9654
  }
7893
9655
  function target2(context, label, descriptor) {
@@ -7910,7 +9672,7 @@ function addStandardPost(context, topology, scene, depth, surface, sceneTemporal
7910
9672
  lane: topology.standardProfile?.renderPath === "deferred" ? "clustered" : "direct",
7911
9673
  temporal: !clearOnly && topology.camera.antialias === "taa",
7912
9674
  meter: topology.output?.autoExposure === true,
7913
- bloom: !clearOnly && topology.camera.bloom === "on" && topology.camera.tonemap !== "none",
9675
+ bloom: !clearOnly && standardBloomAdmitted(topology.camera),
7914
9676
  dof: !clearOnly && topology.camera.depthOfField !== void 0,
7915
9677
  exposure: topology.output?.autoExposure === true,
7916
9678
  whiteBalance: topology.output?.whiteBalance === true,
@@ -7936,6 +9698,32 @@ function addStandardPost(context, topology, scene, depth, surface, sceneTemporal
7936
9698
  const finalPresent = surface.storage;
7937
9699
  let postInput = scene;
7938
9700
  let postTemporal = sceneTemporal;
9701
+ if (!clearOnly && topology.analyticFog === true) {
9702
+ if (depth.sampleCount !== 1)
9703
+ return err$1(
9704
+ new RenderGraphError({
9705
+ code: "resource-resolution-failed",
9706
+ expected: "analytic fog receives single-sample scene depth",
9707
+ hint: "select FXAA, TAA or no AA for analytic fog",
9708
+ detail: { resourceLabel: "scene-depth" }
9709
+ })
9710
+ );
9711
+ const fogged = target2(context, "analytic-fog-color", {
9712
+ format: "rgba16float",
9713
+ size: topology.extent === void 0 ? "surface" : renderExtentSize(topology.extent, "internal"),
9714
+ domain: hdr ? "linear-hdr" : "linear-ldr"
9715
+ });
9716
+ if (!fogged.ok) return fogged;
9717
+ const fog = addTypedFullscreenPass(context.graph, {
9718
+ name: "analytic-fog",
9719
+ shader: "forgeax.analytic-fog",
9720
+ input: postInput,
9721
+ depth,
9722
+ output: fogged.value
9723
+ });
9724
+ if (!fog.ok) return fog;
9725
+ postInput = fogged.value;
9726
+ }
7939
9727
  const autoExposure = context.standardOutput?.autoExposure;
7940
9728
  if (outputPlan.features.exposure && autoExposure === void 0) {
7941
9729
  return err$1(
@@ -7956,7 +9744,7 @@ function addStandardPost(context, topology, scene, depth, surface, sceneTemporal
7956
9744
  return err$1(missingTemporalContributor(topology, "forgeax::standard::taa-history"));
7957
9745
  }
7958
9746
  const resolveTargets = {
7959
- scene,
9747
+ scene: postInput,
7960
9748
  depth,
7961
9749
  currentTemporal: sceneTemporal,
7962
9750
  ...coverage === void 0 ? {} : { coverage },
@@ -7974,6 +9762,15 @@ function addStandardPost(context, topology, scene, depth, surface, sceneTemporal
7974
9762
  postInput = history.currentColor;
7975
9763
  postTemporal = history.currentTemporal;
7976
9764
  }
9765
+ if (outputPlan.features.exposure && autoExposure !== void 0) {
9766
+ const added = addAutoExposureGraphPasses(context, {
9767
+ source: postInput,
9768
+ resources: autoExposure,
9769
+ width: topology.surface.width,
9770
+ height: topology.surface.height
9771
+ });
9772
+ if (!added.ok) return added;
9773
+ }
7977
9774
  if (!clearOnly) {
7978
9775
  const featureTargets = [
7979
9776
  {
@@ -8023,15 +9820,6 @@ function addStandardPost(context, topology, scene, depth, surface, sceneTemporal
8023
9820
  if (!features.ok) return features;
8024
9821
  }
8025
9822
  }
8026
- if (outputPlan.features.exposure && autoExposure !== void 0) {
8027
- const added = addAutoExposureGraphPasses(context, {
8028
- source: postInput,
8029
- resources: autoExposure,
8030
- width: topology.surface.width,
8031
- height: topology.surface.height
8032
- });
8033
- if (!added.ok) return added;
8034
- }
8035
9823
  if (outputPlan.features.dof) {
8036
9824
  const dof = addDepthOfFieldPasses(
8037
9825
  {
@@ -8049,36 +9837,29 @@ function addStandardPost(context, topology, scene, depth, surface, sceneTemporal
8049
9837
  postInput = dof.value;
8050
9838
  }
8051
9839
  if (bloomActive) {
8052
- const composited = target2(
8053
- context,
8054
- STANDARD_BLOOM_TARGET_SPEC.composited.label,
8055
- STANDARD_BLOOM_TARGET_SPEC.composited
8056
- );
9840
+ const outputWidth = topology.extent?.outputWidth ?? topology.surface.width;
9841
+ const outputHeight = topology.extent?.outputHeight ?? topology.surface.height;
9842
+ const bloomSpecs = standardBloomTargetSpecs(outputWidth, outputHeight);
9843
+ const composited = target2(context, bloomSpecs.composited.label, bloomSpecs.composited);
8057
9844
  if (!composited.ok) return composited;
8058
- const bright = target2(
8059
- context,
8060
- STANDARD_BLOOM_TARGET_SPEC.bright.label,
8061
- STANDARD_BLOOM_TARGET_SPEC.bright
8062
- );
8063
- if (!bright.ok) return bright;
8064
- const blurH = target2(
8065
- context,
8066
- STANDARD_BLOOM_TARGET_SPEC.blurH.label,
8067
- STANDARD_BLOOM_TARGET_SPEC.blurH
8068
- );
8069
- if (!blurH.ok) return blurH;
8070
- const blurV = target2(
8071
- context,
8072
- STANDARD_BLOOM_TARGET_SPEC.blurV.label,
8073
- STANDARD_BLOOM_TARGET_SPEC.blurV
8074
- );
8075
- if (!blurV.ok) return blurV;
9845
+ const downsample = [];
9846
+ for (const spec of bloomSpecs.downsample) {
9847
+ const level = target2(context, spec.label, spec);
9848
+ if (!level.ok) return level;
9849
+ downsample.push(level.value);
9850
+ }
9851
+ const upsample = [];
9852
+ for (const spec of bloomSpecs.upsample) {
9853
+ const level = target2(context, spec.label, spec);
9854
+ if (!level.ok) return level;
9855
+ upsample.push(level.value);
9856
+ }
8076
9857
  const bloom = addTypedBloomPasses(context.graph, {
8077
9858
  scene: postInput,
8078
9859
  composited: composited.value,
8079
- bright: bright.value,
8080
- blurH: blurH.value,
8081
- blurV: blurV.value
9860
+ downsample,
9861
+ upsample,
9862
+ levelDimensions: bloomSpecs.levels
8082
9863
  });
8083
9864
  if (!bloom.ok) return bloom;
8084
9865
  postInput = composited.value;
@@ -12082,6 +13863,7 @@ var GpuDrivenProduction = class _GpuDrivenProduction {
12082
13863
  }
12083
13864
  const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
12084
13865
  if (!updated.ok) return updated;
13866
+ this.view.setTelemetrySubmit(input.telemetrySubmit);
12085
13867
  this.telemetryPrepared = true;
12086
13868
  const view = this.view;
12087
13869
  const visibleBuffer = view.visibleBuffer;
@@ -12999,15 +14781,53 @@ var GpuDrivenProduction = class _GpuDrivenProduction {
12999
14781
  this.laneReason = "overflow";
13000
14782
  }
13001
14783
  }
13002
- if (selection === void 0 || telemetryCandidateCount <= 0) return selection;
14784
+ if (selection === void 0) return selection;
13003
14785
  if (filtered === void 0) return selection;
13004
14786
  const preparedByBatchId = new Map(
13005
- filtered.batches.map((prepared) => [prepared.batch.batchId, prepared])
14787
+ filtered.batches.map((prepared) => [
14788
+ prepared.batch.batchId,
14789
+ {
14790
+ batch: prepared.batch,
14791
+ worldKey: prepared.worldKey,
14792
+ primitiveSlot: prepared.primitiveSlot,
14793
+ slotGeneration: prepared.slotGeneration
14794
+ }
14795
+ ])
13006
14796
  );
14797
+ const filteredSlots = this.filteredCache?.slots;
14798
+ const filteredWorldKeys = this.filteredCache?.worldKeys;
14799
+ if (filteredSlots !== void 0) {
14800
+ const slotByPrimitive = new Map(filteredSlots.map((slot) => [slot.slot, slot]));
14801
+ for (const batch of filtered.plan.batches) {
14802
+ if (preparedByBatchId.has(batch.batchId)) continue;
14803
+ const candidate = batch.candidates[0];
14804
+ const slot = candidate === void 0 ? void 0 : slotByPrimitive.get(candidate.primitiveIndex);
14805
+ if (slot === void 0) continue;
14806
+ preparedByBatchId.set(batch.batchId, {
14807
+ batch,
14808
+ worldKey: filteredWorldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId,
14809
+ primitiveSlot: slot.slot,
14810
+ slotGeneration: slot.generation
14811
+ });
14812
+ }
14813
+ }
13007
14814
  const worldSelections = /* @__PURE__ */ new Map();
14815
+ const lodHistogram = /* @__PURE__ */ new Map();
14816
+ let lodCandidateCount = 0;
14817
+ let lodVisible = 0;
13008
14818
  for (const batchSelection of selection.batches) {
13009
14819
  const prepared = preparedByBatchId.get(batchSelection.batchId);
13010
- if (prepared === void 0) return selection;
14820
+ if (prepared === void 0) return void 0;
14821
+ const lodCandidateRows = prepared.batch.candidates.filter(
14822
+ (candidate) => (candidate.lodCoverages?.length ?? 0) > 1
14823
+ );
14824
+ if (lodCandidateRows.length === 0) continue;
14825
+ if (lodCandidateRows.length !== batchSelection.candidateCount) return void 0;
14826
+ lodCandidateCount += batchSelection.candidateCount;
14827
+ lodVisible += batchSelection.visible;
14828
+ for (const row of batchSelection.lodHistogram) {
14829
+ lodHistogram.set(row.level, (lodHistogram.get(row.level) ?? 0) + row.count);
14830
+ }
13011
14831
  let aggregate = worldSelections.get(prepared.worldKey);
13012
14832
  if (aggregate === void 0) {
13013
14833
  aggregate = {
@@ -13033,8 +14853,12 @@ var GpuDrivenProduction = class _GpuDrivenProduction {
13033
14853
  }
13034
14854
  return {
13035
14855
  ...selection,
13036
- candidateCount: telemetryCandidateCount,
13037
- occluded: Math.max(0, telemetryCandidateCount - selection.visible),
14856
+ candidateCount: lodCandidateCount,
14857
+ visible: lodVisible,
14858
+ occluded: Math.max(0, lodCandidateCount - lodVisible),
14859
+ lodHistogram: Object.freeze(
14860
+ [...lodHistogram.entries()].sort(([left], [right]) => left - right).map(([level, count]) => Object.freeze({ level, count }))
14861
+ ),
13038
14862
  worldSelections: Object.freeze(
13039
14863
  [...worldSelections.values()].map(
13040
14864
  (aggregate) => Object.freeze({
@@ -13594,6 +15418,49 @@ function projectDepthOfFieldInspection(input) {
13594
15418
  function requestedEqual(requested, params) {
13595
15419
  return requested.focusDistance === params.focusDistance && requested.fStop === params.fStop && requested.sensorHeight === params.sensorHeight && requested.maxRadiusPixels === params.maxRadiusPixels && requested.quality === params.quality && requested.blurSide === params.blurSide;
13596
15420
  }
15421
+
15422
+ // src/features/error-reporting.ts
15423
+ var PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES = 60;
15424
+ function shouldReportPendingRenderFeatureError(lastReportedFrame, key, frameNumber, intervalFrames = PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES) {
15425
+ const lastReported = lastReportedFrame.get(key);
15426
+ if (lastReported !== void 0 && frameNumber - lastReported < intervalFrames) return false;
15427
+ lastReportedFrame.set(key, frameNumber);
15428
+ return true;
15429
+ }
15430
+ function reportPendingRenderFeatureErrors(errors, frameNumber, lastReportedFrame, errorRegistry) {
15431
+ for (const error of errors) {
15432
+ if (!isPendingRenderFeaturePreparation(error)) {
15433
+ errorRegistry.fire(error);
15434
+ continue;
15435
+ }
15436
+ if (isSilentRenderFeatureWarmup(error)) continue;
15437
+ const key = pendingRenderFeatureErrorKey(error);
15438
+ if (shouldReportPendingRenderFeatureError(lastReportedFrame, key, frameNumber)) {
15439
+ errorRegistry.fire(error);
15440
+ }
15441
+ }
15442
+ }
15443
+ function isPendingRenderFeaturePreparation(error) {
15444
+ if (!("detail" in error)) return false;
15445
+ switch (error.code) {
15446
+ case "render-feature-stage-failed":
15447
+ case "render-feature-preparation-failed":
15448
+ case "render-feature-prepared-state-mismatch":
15449
+ case "render-feature-draw-recording-failed":
15450
+ return error.detail.recovery === "next-frame";
15451
+ default:
15452
+ return false;
15453
+ }
15454
+ }
15455
+ function isSilentRenderFeatureWarmup(error) {
15456
+ return error.code === "render-feature-preparation-failed" && error.detail.reason.startsWith("rhi-not-available:");
15457
+ }
15458
+ function pendingRenderFeatureErrorKey(error) {
15459
+ const detail = "detail" in error ? error.detail : void 0;
15460
+ const featureIdentity = detail !== void 0 && "featureIdentity" in detail && typeof detail.featureIdentity === "string" ? detail.featureIdentity : "";
15461
+ const order = detail !== void 0 && "order" in detail && typeof detail.order === "number" ? detail.order : -1;
15462
+ return `${error.code}\0${featureIdentity}\0${order}\0${error.expected}\0${error.hint}`;
15463
+ }
13597
15464
  function createFeatureNoisePixels() {
13598
15465
  const pixels = new Uint8Array(64 * 64 * 4);
13599
15466
  let state = 1831565813;
@@ -13969,6 +15836,17 @@ function createRenderFeatureGpuWorkSession(input) {
13969
15836
  pipelineMap.set(entryPoint, pipeline.value);
13970
15837
  }
13971
15838
  const reference = Object.freeze({ generation: input.generation });
15839
+ const textureBindings = /* @__PURE__ */ new Map();
15840
+ for (const entry of descriptor.bindings?.[0]?.entries ?? []) {
15841
+ if (entry.texture !== void 0) {
15842
+ textureBindings.set(entry.binding, "sampled");
15843
+ } else if (entry.storageTexture !== void 0) {
15844
+ textureBindings.set(
15845
+ entry.binding,
15846
+ entry.storageTexture.access === "write-only" ? "storage-write" : entry.storageTexture.access === "read-only" ? "storage-read" : "storage-read-write"
15847
+ );
15848
+ }
15849
+ }
13972
15850
  const item = {
13973
15851
  name,
13974
15852
  descriptor: cloneProgramDescriptor(descriptor),
@@ -13980,6 +15858,7 @@ function createRenderFeatureGpuWorkSession(input) {
13980
15858
  (entry) => entry.buffer === void 0 ? [] : [[entry.binding, entry.buffer.type ?? "uniform"]]
13981
15859
  )
13982
15860
  ),
15861
+ textureBindings,
13983
15862
  ...bindGroupLayout === void 0 ? {} : { bindGroupLayout }
13984
15863
  };
13985
15864
  programs.set(name, item);
@@ -14277,7 +16156,7 @@ function createRenderFeatureGpuWorkSession(input) {
14277
16156
  }
14278
16157
  entryItems.push({
14279
16158
  binding: entry.binding,
14280
- resource: { kind: "textureView", value: item3.resource },
16159
+ ...item3.resource === void 0 ? {} : { resource: { kind: "textureView", value: item3.resource } },
14281
16160
  textureView: item3
14282
16161
  });
14283
16162
  continue;
@@ -14305,7 +16184,7 @@ function createRenderFeatureGpuWorkSession(input) {
14305
16184
  entries: entryItems.map((entry) => [
14306
16185
  entry.binding,
14307
16186
  entry.buffer?.name ?? entry.textureView?.name ?? entry.sampler?.name,
14308
- entry.resource.kind
16187
+ entry.resource?.kind ?? "late-texture"
14309
16188
  ])
14310
16189
  });
14311
16190
  const prior = bindings.get(name) ?? parkedBindings.get(name)?.item;
@@ -14335,7 +16214,22 @@ function createRenderFeatureGpuWorkSession(input) {
14335
16214
  )
14336
16215
  );
14337
16216
  }
14338
- const created = input.device.createBindGroup({
16217
+ const hasLateTextures = entryItems.some(
16218
+ (entry) => entry.textureView?.logicalTarget !== void 0
16219
+ );
16220
+ const hasMissingResource = entryItems.some((entry) => entry.resource === void 0);
16221
+ if (hasMissingResource && !hasLateTextures) {
16222
+ return err$1(
16223
+ failure4(
16224
+ input.featureIdentity,
16225
+ "prepare-gpu-bindings",
16226
+ "bindings",
16227
+ name,
16228
+ "texture-view-unavailable"
16229
+ )
16230
+ );
16231
+ }
16232
+ const created = hasLateTextures ? void 0 : input.device.createBindGroup({
14339
16233
  label: name,
14340
16234
  layout,
14341
16235
  entries: entryItems.map((entry) => ({
@@ -14343,7 +16237,7 @@ function createRenderFeatureGpuWorkSession(input) {
14343
16237
  resource: entry.resource
14344
16238
  }))
14345
16239
  });
14346
- if (!created.ok) {
16240
+ if (created !== void 0 && !created.ok) {
14347
16241
  return err$1(
14348
16242
  failure4(
14349
16243
  input.featureIdentity,
@@ -14366,7 +16260,10 @@ function createRenderFeatureGpuWorkSession(input) {
14366
16260
  buffers: entryItems.flatMap(
14367
16261
  (entry) => entry.buffer === void 0 ? [] : [{ binding: entry.binding, item: entry.buffer }]
14368
16262
  ),
14369
- bindGroup: created.value
16263
+ // A bind group with semantic targets is created once the graph resolves
16264
+ // the current generation's physical views. The placeholder is never
16265
+ // encoded because resolveComputePass installs resolveBindGroup.
16266
+ bindGroup: created?.value
14370
16267
  };
14371
16268
  bindings.set(name, item);
14372
16269
  bindingRefs.set(reference, item);
@@ -14552,16 +16449,20 @@ function createRenderFeatureGpuWorkSession(input) {
14552
16449
  accesses.add("indirect-read");
14553
16450
  resolvedBuffers.set(item, accesses);
14554
16451
  }
16452
+ const sampledTargets = /* @__PURE__ */ new Set();
16453
+ const storageTargets = /* @__PURE__ */ new Set();
16454
+ for (const item of dispatchBindings) {
16455
+ for (const entry of item?.entries ?? []) {
16456
+ const target5 = entry.textureView?.logicalTarget;
16457
+ if (target5 === void 0) continue;
16458
+ const role = program.textureBindings.get(entry.binding) ?? "sampled";
16459
+ if (role === "sampled") sampledTargets.add(target5);
16460
+ else storageTargets.add(target5);
16461
+ }
16462
+ }
14555
16463
  return ok$1({
14556
- sampledTargets: [
14557
- ...new Set(
14558
- dispatchBindings.flatMap(
14559
- (item) => item?.entries.flatMap(
14560
- (entry) => entry.textureView?.logicalTarget === void 0 ? [] : [entry.textureView.logicalTarget]
14561
- ) ?? []
14562
- )
14563
- )
14564
- ],
16464
+ sampledTargets: [...sampledTargets],
16465
+ storageTargets: [...storageTargets],
14565
16466
  buffers: [...resolvedBuffers].flatMap(
14566
16467
  ([item, accesses]) => [...accesses].map((access) => ({
14567
16468
  name: item.name,
@@ -15782,9 +17683,7 @@ function collectGpuDrivenMaterialArtifacts({
15782
17683
  specializationKey: sceneIndexProgramKey
15783
17684
  };
15784
17685
  const receipt = selectedIdentityArtifact?.receipt;
15785
- const shadowVertexInputsMatch = pass !== "shadow" || receipt !== void 0 && receipt.vertexInputs.length === prepared.vertexInputs.length && receipt.vertexInputs.every(
15786
- (input, index) => input.semantic === prepared.vertexInputs[index]?.semantic && input.location === prepared.vertexInputs[index]?.location && input.format === prepared.vertexInputs[index]?.format
15787
- );
17686
+ const shadowVertexInputsMatch = pass !== "shadow" || receipt !== void 0 && hasRequiredVertexInputs(receipt.vertexInputs, prepared.vertexInputs);
15788
17687
  if (selectedIdentityArtifact === void 0 || receipt === void 0 || !shadowVertexInputsMatch || pass !== "shadow" && (receipt.receiptIdentity !== prepared.receiptIdentity || receipt.generation !== prepared.receiptGeneration)) {
15789
17688
  continue;
15790
17689
  }
@@ -15853,6 +17752,29 @@ function inspectStandardLighting(input) {
15853
17752
  }
15854
17753
  return inspectStandardClusterTransport(input.transport, input.prepared);
15855
17754
  }
17755
+ function encodeAnalyticFogParams(fog, camera) {
17756
+ const projection = computeProjectionMatrix(camera);
17757
+ const jitter = camera.temporal?.currentJitterUv;
17758
+ if (jitter !== void 0) {
17759
+ for (let column = 0; column < 4; column++) {
17760
+ projection[column * 4] = (projection[column * 4] ?? 0) + jitter[0] * 2 * (projection[column * 4 + 3] ?? 0);
17761
+ projection[column * 4 + 1] = (projection[column * 4 + 1] ?? 0) - jitter[1] * 2 * (projection[column * 4 + 3] ?? 0);
17762
+ }
17763
+ }
17764
+ const inverse = mat4.invert(
17765
+ mat4.create(),
17766
+ mat4.multiply(mat4.create(), projection, computeViewMatrix(camera))
17767
+ );
17768
+ const data = new Float32Array(28);
17769
+ data.set(inverse, 0);
17770
+ data.set(camera.position, 16);
17771
+ data.set([...fog.color, fog.density], 20);
17772
+ data.set(
17773
+ [fog.heightFalloff, fog.maxOpacity, camera.near, camera.projection === "orthographic" ? 1 : 0],
17774
+ 24
17775
+ );
17776
+ return new Uint8Array(data.buffer);
17777
+ }
15856
17778
  function uploadMaterialUniforms(c) {
15857
17779
  const {
15858
17780
  runtime,
@@ -16015,6 +17937,12 @@ function resolveVolumeTemporalReset(previous, next, options = {}) {
16015
17937
  if (previous.lightShadowRevision !== next.lightShadowRevision) {
16016
17938
  return { reset: true, reason: "light-revision" };
16017
17939
  }
17940
+ if (previous.worldTimeSeconds !== next.worldTimeSeconds) {
17941
+ const delta = (next.worldTimeSeconds ?? NaN) - (previous.worldTimeSeconds ?? NaN);
17942
+ if (!Number.isFinite(delta) || delta < 0 || delta > 0.1) {
17943
+ return { reset: true, reason: "simulation-time" };
17944
+ }
17945
+ }
16018
17946
  if (previous.densityGeneration !== next.densityGeneration) {
16019
17947
  return { reset: true, reason: "density-generation" };
16020
17948
  }
@@ -16073,6 +18001,7 @@ function createTemporalView(input) {
16073
18001
  previousUnjitteredViewProjection: input.previousUnjitteredViewProjection,
16074
18002
  currentCameraPosition: input.currentCameraPosition,
16075
18003
  previousCameraPosition: input.previousCameraPosition,
18004
+ sampleTimeSeconds: input.sampleTimeSeconds,
16076
18005
  resetReason: input.resetReason
16077
18006
  });
16078
18007
  const taa = input.antialias === "taa";
@@ -16090,6 +18019,7 @@ function createTemporalView(input) {
16090
18019
  previousUnjitteredViewProjection: input.previousUnjitteredViewProjection,
16091
18020
  currentCameraPosition: input.currentCameraPosition,
16092
18021
  previousCameraPosition: input.previousCameraPosition,
18022
+ sampleTimeSeconds: input.sampleTimeSeconds,
16093
18023
  currentProjection: taa ? "jittered" : "unjittered",
16094
18024
  previousProjection: "unjittered",
16095
18025
  historyRequired: taa,
@@ -16097,8 +18027,8 @@ function createTemporalView(input) {
16097
18027
  });
16098
18028
  }
16099
18029
  function resolveTemporalReset(previous, next) {
16100
- if (next.mode !== "taa") return void 0;
16101
- if (previous === void 0 || previous.mode !== "taa") return "first-frame";
18030
+ if (previous === void 0) return "first-frame";
18031
+ if (next.mode === "taa" && previous.mode !== "taa") return "first-frame";
16102
18032
  const before = previous.input;
16103
18033
  const after = next.input;
16104
18034
  if (before.width !== after.width || before.height !== after.height) return "resize";
@@ -16125,6 +18055,12 @@ function graphExecutionPhase(passName) {
16125
18055
  return "record/graph-execute/ssr-reflection-mip";
16126
18056
  }
16127
18057
  if (passName === "ssr-compose") return "record/graph-execute/ssr-compose";
18058
+ if (passName.startsWith("bloom-downsample-")) {
18059
+ return "record/graph-execute/bloom-downsample";
18060
+ }
18061
+ if (passName.startsWith("bloom-upsample-")) {
18062
+ return "record/graph-execute/bloom-upsample";
18063
+ }
16128
18064
  switch (passName) {
16129
18065
  case "cluster-binner-upload":
16130
18066
  case "cluster-membership-producer":
@@ -16154,12 +18090,10 @@ function graphExecutionPhase(passName) {
16154
18090
  return "record/graph-execute/main";
16155
18091
  case "fxaa":
16156
18092
  return "record/graph-execute/fxaa";
16157
- case "bloom-bright":
16158
- return "record/graph-execute/bloom-bright";
16159
- case "bloom-blur-h":
16160
- return "record/graph-execute/bloom-blur-h";
16161
- case "bloom-blur-v":
16162
- return "record/graph-execute/bloom-blur-v";
18093
+ case "bloom-downsample":
18094
+ return "record/graph-execute/bloom-downsample";
18095
+ case "bloom-upsample":
18096
+ return "record/graph-execute/bloom-upsample";
16163
18097
  case "bloom-composite":
16164
18098
  return "record/graph-execute/bloom-composite";
16165
18099
  default:
@@ -16331,19 +18265,27 @@ var RenderFeatureComputeGraphProjection = class {
16331
18265
  addPass(name, featureIdentity, order, work) {
16332
18266
  const accesses = [];
16333
18267
  const targets = /* @__PURE__ */ new Map();
16334
- for (const handle of work.sampledTargets ?? []) {
18268
+ const sampledTargets = new Set(work.sampledTargets ?? []);
18269
+ const storageTargets = new Set(work.storageTargets ?? []);
18270
+ for (const handle of [...sampledTargets, ...storageTargets]) {
16335
18271
  let target5 = this.resolveTarget?.(handle);
16336
18272
  if (target5 === void 0)
16337
18273
  return err$1(
16338
18274
  new RenderFeatureStageFailedError(featureIdentity, order, "record", "renderer-recover")
16339
18275
  );
16340
- if (handle.kind === "scene-depth") {
18276
+ if (typeof handle !== "string" && handle.kind === "scene-depth") {
16341
18277
  const view = this.builder.view(target5.texture, { aspect: "depth-only" });
16342
18278
  if (!view.ok) return view;
16343
18279
  target5 = { ...target5, view: view.value };
16344
18280
  }
16345
18281
  targets.set(handle, target5);
16346
- accesses.push({ resource: target5.view, usage: "sampled-read" });
18282
+ if (sampledTargets.has(handle) && storageTargets.has(handle)) {
18283
+ accesses.push({ resource: target5.view, usage: "sampled-storage-read-write" });
18284
+ } else if (storageTargets.has(handle)) {
18285
+ accesses.push({ resource: target5.view, usage: "storage-write" });
18286
+ } else {
18287
+ accesses.push({ resource: target5.view, usage: "sampled-read" });
18288
+ }
16347
18289
  }
16348
18290
  for (const resource of work.buffers) {
16349
18291
  const imported = importRenderFeatureGraphBuffer(
@@ -16796,6 +18738,7 @@ function projectRenderFeaturePlans(builder, executions, options) {
16796
18738
  pass.resolvedGpuCompute
16797
18739
  );
16798
18740
  if (!added.ok) return added;
18741
+ state.projected.add(pass);
16799
18742
  continue;
16800
18743
  }
16801
18744
  if (pass.graphics !== void 0 && pass.graphicsState !== void 0 && pass.resolvedGraphics !== void 0) {
@@ -16808,6 +18751,7 @@ function projectRenderFeaturePlans(builder, executions, options) {
16808
18751
  pass.resolvedGraphics
16809
18752
  );
16810
18753
  if (!added.ok) return added;
18754
+ state.projected.add(pass);
16811
18755
  continue;
16812
18756
  }
16813
18757
  return err$1(
@@ -17073,7 +19017,12 @@ function importSsrHistoryTargets(graph) {
17073
19017
  };
17074
19018
  }
17075
19019
  function isMotionBlurTemporalDemand(motionBlur) {
17076
- return motionBlur?.shutterAngle !== void 0 && motionBlur.shutterAngle > 0;
19020
+ return motionBlur !== void 0 && motionBlurTemporalDemand({
19021
+ shutterAngle: motionBlur.shutterAngle,
19022
+ maxRadiusPixels: motionBlur.maxRadiusPixels,
19023
+ sampleCount: motionBlur.sampleCount,
19024
+ targetFps: motionBlur.targetFps ?? 60
19025
+ });
17077
19026
  }
17078
19027
  function renderTargetMipCount(work) {
17079
19028
  const { descriptor } = work.physical;
@@ -17644,7 +19593,7 @@ function commitFeatureCandidate(internals, candidate) {
17644
19593
  state.plans = candidate.plans;
17645
19594
  state.fullscreenEffects = candidate.fullscreenEffects;
17646
19595
  }
17647
- function topologyOf(internals, frameState, pipelineState, camera, clearOnly, lights, width, height, shadowMapSize, gpuDriven, featureGraphCandidate, featurePlanRevision, cubeCaptureState, transmissionDemand, volumetricFog, standardLighting, ssr, renderExtent) {
19596
+ function topologyOf(internals, frameState, pipelineState, camera, clearOnly, lights, width, height, shadowMapSize, gpuDriven, featureGraphCandidate, featurePlanRevision, cubeCaptureState, transmissionDemand, volumetricFog, standardLighting, ssr, renderExtent, analyticFog) {
17648
19597
  const cascadeCount = Math.max(1, Math.min(4, Math.round(lights.cascadeCount ?? 1)));
17649
19598
  const surfaceFormats = resolveSurfaceFormatPair(
17650
19599
  internals.device.caps.backendKind,
@@ -17688,6 +19637,7 @@ function topologyOf(internals, frameState, pipelineState, camera, clearOnly, lig
17688
19637
  reflectionFallback: { enabled: frameState.reflectionFallbackDemand === true },
17689
19638
  ssr,
17690
19639
  ...renderExtent === void 0 ? {} : { extent: renderExtent },
19640
+ analyticFog: !clearOnly && analyticFog,
17691
19641
  surface: {
17692
19642
  width,
17693
19643
  height,
@@ -17698,7 +19648,11 @@ function topologyOf(internals, frameState, pipelineState, camera, clearOnly, lig
17698
19648
  camera: {
17699
19649
  tonemap: camera.tonemap,
17700
19650
  antialias: camera.antialias,
17701
- bloom: camera.bloom,
19651
+ // Bloom intensity is a frame parameter, not a graph identity. Fold
19652
+ // only its zero/non-zero admission into the closed topology switch so
19653
+ // positive intensity changes update the recorder UBO in place.
19654
+ bloom: standardBloomAdmitted(camera) ? "on" : "off",
19655
+ bloomIntensity: camera.bloomIntensity,
17702
19656
  ...depthOfField === void 0 ? {} : { depthOfField }
17703
19657
  },
17704
19658
  atmosphere: !clearOnly && frameState.environmentFrame?.source.kind === "atmosphere",
@@ -17976,7 +19930,7 @@ function settleDepthOfFieldGraphCandidate(frameState, submitted) {
17976
19930
  frameState.depthOfFieldPreviousStandardLightingGraphSignature = "";
17977
19931
  frameState.depthOfFieldCandidateGraph = null;
17978
19932
  }
17979
- function ensureCompiledFrameGraph(internals, frameState, pipelineState, camera, lights, width, height, shadowMapSize, gpuDriven, featureGraphCandidate, cubeCaptureState, clearOnly = false, transmissionDemand, occlusion, volumetricFog, volumeTopologyCandidate = false, standardLighting, onRecoveryError, ssr, renderExtent) {
19933
+ function ensureCompiledFrameGraph(internals, frameState, pipelineState, camera, lights, width, height, shadowMapSize, gpuDriven, featureGraphCandidate, cubeCaptureState, clearOnly = false, transmissionDemand, occlusion, volumetricFog, volumeTopologyCandidate = false, standardLighting, onRecoveryError, ssr, renderExtent, analyticFog = false) {
17980
19934
  const featureMetrics = mutableFeatureGraphInspection(internals);
17981
19935
  featureMetrics.ensureCalls += 1;
17982
19936
  const requestedStandardLightingSignature = standardLightingTopologySignature(standardLighting);
@@ -18027,7 +19981,8 @@ function ensureCompiledFrameGraph(internals, frameState, pipelineState, camera,
18027
19981
  volumetricFog,
18028
19982
  standardLighting,
18029
19983
  ssr,
18030
- renderExtent
19984
+ renderExtent,
19985
+ analyticFog
18031
19986
  );
18032
19987
  if (!topologyResult.ok) {
18033
19988
  featureMetrics.buildFailures += 1;
@@ -18040,6 +19995,10 @@ function ensureCompiledFrameGraph(internals, frameState, pipelineState, camera,
18040
19995
  }
18041
19996
  const requestedPostEffects = topologyResult.value.config?.postEffects ?? [];
18042
19997
  const featurePostProcessEntries = featureGraphCandidate?.fullscreenEffects ?? getRenderFeatureGraphState(internals).fullscreenEffects;
19998
+ const fullscreenFeatureIds = /* @__PURE__ */ new Set([
19999
+ ...featurePostProcessEntries.keys(),
20000
+ "forgeax.motion-blur"
20001
+ ]);
18043
20002
  const postProcessAdmission = resolvePostProcessChainAdmission(
18044
20003
  requestedPostEffects,
18045
20004
  (identity) => {
@@ -18076,8 +20035,15 @@ function ensureCompiledFrameGraph(internals, frameState, pipelineState, camera,
18076
20035
  postEffects: []
18077
20036
  }
18078
20037
  } : topologyResult.value;
20038
+ const topologyIdentity = {
20039
+ ...topology,
20040
+ camera: {
20041
+ ...topology.camera,
20042
+ bloomIntensity: void 0
20043
+ }
20044
+ };
18079
20045
  const key = JSON.stringify({
18080
- topology,
20046
+ topology: topologyIdentity,
18081
20047
  // Observation domains are an explicit per-frame graph demand. They are
18082
20048
  // part of the cache identity so a request made after the ordinary graph
18083
20049
  // was compiled can admit the matching tone/output capture boundaries on
@@ -18174,7 +20140,6 @@ function ensureCompiledFrameGraph(internals, frameState, pipelineState, camera,
18174
20140
  return lastKnownGood();
18175
20141
  }
18176
20142
  const ssrHistory = ssr?.status === "admitted" ? importSsrHistoryTargets(builder) : void 0;
18177
- let projectedFeatures = false;
18178
20143
  const featureProjection = createRenderFeatureProjectionState();
18179
20144
  let featureShadows;
18180
20145
  let projectedGpuDriven = false;
@@ -18227,11 +20192,12 @@ function ensureCompiledFrameGraph(internals, frameState, pipelineState, camera,
18227
20192
  featureProjection
18228
20193
  ),
18229
20194
  contributeFeatures: (targets, semanticTargets = [], namedTargets = {}, standardSurfaceAccesses = []) => {
18230
- if (projectedFeatures) return ok$1(void 0);
18231
- projectedFeatures = true;
18232
- if (projectedFeaturePlans.value.length === 0) return ok$1(void 0);
20195
+ const plans = Object.keys(namedTargets).length === 0 ? projectedFeaturePlans.value.filter(
20196
+ (execution) => !fullscreenFeatureIds.has(execution.featureIdentity)
20197
+ ) : projectedFeaturePlans.value;
20198
+ if (plans.length === 0) return ok$1(void 0);
18233
20199
  const resolveTarget = targetResolver(targets, semanticTargets, namedTargets);
18234
- const projected = projectRenderFeaturePlans(builder, projectedFeaturePlans.value, {
20200
+ const projected = projectRenderFeaturePlans(builder, plans, {
18235
20201
  state: featureProjection,
18236
20202
  resolveTarget,
18237
20203
  standardSurfaceAccesses,
@@ -18348,6 +20314,70 @@ function ensureCompiledFrameGraph(internals, frameState, pipelineState, camera,
18348
20314
  }
18349
20315
  return compiled.value;
18350
20316
  }
20317
+ function deriveMotionBlurExecutionReceipt(internals, frameState) {
20318
+ const graph = frameState.compiledFrameGraph;
20319
+ const deviceGeneration = internals.deviceScope?.generation;
20320
+ if (graph === null || deviceGeneration === void 0) return void 0;
20321
+ const info = graph.inspect();
20322
+ const graphPasses = new Map(info.passes.map((pass) => [pass.name, pass]));
20323
+ const summaryGraphPass = graphPasses.get("motion-blur-tile-summary");
20324
+ const reconstructGraphPass = graphPasses.get("motion-blur");
20325
+ if (summaryGraphPass?.kind !== "compute" || reconstructGraphPass?.kind !== "compute") {
20326
+ return void 0;
20327
+ }
20328
+ const planned = getRenderFeatureGraphState(internals).plans.find(
20329
+ (candidate) => candidate.featureIdentity === "forgeax.motion-blur"
20330
+ );
20331
+ const execution = planned === void 0 ? void 0 : getRenderFeaturePlanExecutionProjection(planned);
20332
+ if (execution === void 0 || execution.featureIdentity !== "forgeax.motion-blur") {
20333
+ return void 0;
20334
+ }
20335
+ const executionPass = (name) => execution.passes.find((pass) => pass.name === name);
20336
+ const summary = executionPass("motion-blur-tile-summary");
20337
+ const reconstruct = executionPass("motion-blur");
20338
+ if (summary?.gpuCompute === void 0 || reconstruct?.gpuCompute === void 0 || summary.resolvedGpuCompute === void 0 || reconstruct.resolvedGpuCompute === void 0) {
20339
+ return void 0;
20340
+ }
20341
+ const directWorkgroups = (pass) => {
20342
+ const dispatch = pass.dispatches[0];
20343
+ if (dispatch === void 0 || !("workgroups" in dispatch)) return void 0;
20344
+ return dispatch.workgroups;
20345
+ };
20346
+ const summaryWorkgroups = directWorkgroups(summary.resolvedGpuCompute);
20347
+ const reconstructWorkgroups = directWorkgroups(reconstruct.resolvedGpuCompute);
20348
+ if (summaryWorkgroups === void 0 || reconstructWorkgroups === void 0) return void 0;
20349
+ const outputResource = info.resources.find(
20350
+ (resource) => resource.label === "motion-blurred-color" || reconstructGraphPass.accesses.some(
20351
+ (access) => access.resource === resource.label && access.usage === "storage-write"
20352
+ )
20353
+ );
20354
+ if (outputResource?.descriptor.kind !== "texture" || outputResource.descriptor.format !== "rgba16float") {
20355
+ return void 0;
20356
+ }
20357
+ const summaryReceipt = {
20358
+ name: "motion-blur-tile-summary",
20359
+ kind: "compute",
20360
+ entryPoint: "tile_summary",
20361
+ workgroups: summaryWorkgroups
20362
+ };
20363
+ const reconstructReceipt = {
20364
+ name: "motion-blur",
20365
+ kind: "compute",
20366
+ entryPoint: "reconstruct",
20367
+ workgroups: reconstructWorkgroups
20368
+ };
20369
+ return Object.freeze({
20370
+ frameId: frameState.frameNumber,
20371
+ deviceGeneration,
20372
+ // RenderGraphBuilder generations are process-global. The renderer's
20373
+ // frame-state generation is the identity used by inspection and by the
20374
+ // same-frame admission check below, so retain that owner-scoped value.
20375
+ graphGeneration: frameState.graphGeneration,
20376
+ program: "motion-blur-compute",
20377
+ outputFormat: "rgba16float",
20378
+ passes: [summaryReceipt, reconstructReceipt]
20379
+ });
20380
+ }
18351
20381
  function executeCompiledFrameGraph(internals, frameState, frame, encoder, runPass, frameHooks, readbackFaces, onSubmitted, timingCapture) {
18352
20382
  const legacySubmitCommit = typeof frameHooks === "function" ? frameHooks : void 0;
18353
20383
  const resolvedFrameHooks = typeof frameHooks === "function" ? void 0 : frameHooks;
@@ -18366,6 +20396,7 @@ function executeCompiledFrameGraph(internals, frameState, frame, encoder, runPas
18366
20396
  rejectTemporalFrame();
18367
20397
  return false;
18368
20398
  }
20399
+ const motionBlurExecution = deriveMotionBlurExecutionReceipt(internals, frameState);
18369
20400
  const legacyTimingInstrumentation = timingCapture === void 0 ? void 0 : createTimingInstrumentation(timingCapture);
18370
20401
  const timingHost = internals;
18371
20402
  timingHost.gpuPassTimingCapture = void 0;
@@ -18894,6 +20925,11 @@ function executeCompiledFrameGraph(internals, frameState, frame, encoder, runPas
18894
20925
  commit: () => {
18895
20926
  resolvedFrameHooks?.onSubmitted?.();
18896
20927
  legacySubmitCommit?.();
20928
+ if (motionBlurExecution === void 0) {
20929
+ delete frameState.motionBlurExecution;
20930
+ } else {
20931
+ frameState.motionBlurExecution = motionBlurExecution;
20932
+ }
18897
20933
  if (frameState.temporalFrameInput !== void 0) {
18898
20934
  const temporal = frameState.temporalFrameTransaction.commit({ accepted: true });
18899
20935
  if (temporal.ok) {
@@ -19663,6 +21699,7 @@ function volumeTemporalSignature(fog, prepared, camera, lights, width, height) {
19663
21699
  densityGeneration: prepared.densityGeneration,
19664
21700
  width,
19665
21701
  height,
21702
+ ...fog.worldTimeSeconds === void 0 ? {} : { worldTimeSeconds: fog.worldTimeSeconds },
19666
21703
  ...fog.lightKind === void 0 ? {} : { lightKind: fog.lightKind },
19667
21704
  ...fog.lightEntity === void 0 ? {} : { lightEntity: fog.lightEntity },
19668
21705
  ...fog.pointLightEntity === void 0 ? {} : { pointLightEntity: fog.pointLightEntity },
@@ -19721,7 +21758,7 @@ function prepareVolumetricFogFrame(internals, frameState, world, worlds, fog, li
19721
21758
  }
19722
21759
  return void 0;
19723
21760
  }
19724
- const params = new Float32Array(32);
21761
+ const params = new Float32Array(36);
19725
21762
  params.set(fog.fog.bounds.min, 0);
19726
21763
  params.set(fog.fog.bounds.max, 4);
19727
21764
  params.set(fog.fog.extinction, 8);
@@ -19733,7 +21770,7 @@ function prepareVolumetricFogFrame(internals, frameState, world, worlds, fog, li
19733
21770
  const lightColor = selectedSpot?.color ?? lights.directional?.color ?? vec3.create(0, 0, 0);
19734
21771
  if (lightDirection === void 0 || lightColor === void 0) return void 0;
19735
21772
  params.set(lightDirection, 20);
19736
- params[23] = frameState.frameNumber % 1024;
21773
+ params[23] = 0;
19737
21774
  const selectedPointIndex = selectedPoint === void 0 ? -1 : lights.point.findIndex((light) => light.entity === selectedPoint.entity);
19738
21775
  const selectedSpotIndex = selectedSpot === void 0 ? -1 : lights.spot.findIndex((light) => light.entity === selectedSpot.entity);
19739
21776
  const selectedSpotClusterIndex = selectedSpotIndex < 0 ? -1 : lights.point.length + selectedSpotIndex;
@@ -19744,6 +21781,7 @@ function prepareVolumetricFogFrame(internals, frameState, world, worlds, fog, li
19744
21781
  params[29] = fog.fog.anisotropy;
19745
21782
  params[30] = selectedPoint !== void 0 && selectedSpot !== void 0 ? 3 : selectedPoint !== void 0 ? 1 : selectedSpot !== void 0 ? 2 : 0;
19746
21783
  params[31] = frameState.temporalFrame !== void 0 && frameState.temporalFrame.resetReason === "none" ? 1 : 0;
21784
+ params[32] = fog.worldTimeSeconds ?? 0;
19747
21785
  const paramsBuffer = stageVolumetricFogParams(internals, frameState, params);
19748
21786
  if (paramsBuffer === void 0) return void 0;
19749
21787
  return {
@@ -19847,6 +21885,8 @@ function mapSsrHistoryResetReason(reason2) {
19847
21885
  case "environment-change":
19848
21886
  case "fog-change":
19849
21887
  return "reflection-generation";
21888
+ case "time-discontinuity":
21889
+ return "camera-cut";
19850
21890
  case "device-recover":
19851
21891
  return "device-recovery";
19852
21892
  case "submit-failure":
@@ -19854,6 +21894,27 @@ function mapSsrHistoryResetReason(reason2) {
19854
21894
  return void 0;
19855
21895
  }
19856
21896
  }
21897
+ function frameClockSample(world, worlds, camera, sampleTimeSeconds) {
21898
+ const owner = worlds[camera.worldId ?? 0] ?? world;
21899
+ const invalidSampleTime = sampleTimeSeconds !== void 0 && !Number.isFinite(sampleTimeSeconds);
21900
+ try {
21901
+ const clock = owner.getResource(Time);
21902
+ return {
21903
+ deltaSeconds: Number.isFinite(clock.delta) ? Math.max(0, clock.delta) : 0,
21904
+ // Keep the raw host sample even when ECS accepted only a clamped
21905
+ // simulation delta. Temporal admission compares this value with the
21906
+ // last successfully submitted sample and therefore sees real hitches.
21907
+ elapsedSeconds: sampleTimeSeconds === void 0 ? Number.isFinite(clock.elapsed) ? clock.elapsed : void 0 : Number.isFinite(sampleTimeSeconds) ? sampleTimeSeconds : void 0,
21908
+ invalidSampleTime
21909
+ };
21910
+ } catch {
21911
+ return {
21912
+ deltaSeconds: 0,
21913
+ elapsedSeconds: Number.isFinite(sampleTimeSeconds) ? sampleTimeSeconds : void 0,
21914
+ invalidSampleTime
21915
+ };
21916
+ }
21917
+ }
19857
21918
  function stageSsrHistoryCandidate(internals, frameState, camera, width, height, admission, temporalView) {
19858
21919
  const previousAdmission = frameState.ssrSpatialAdmission;
19859
21920
  frameState.ssrSpatialAdmission = admission;
@@ -20081,7 +22142,7 @@ function updateVolumetricFogInspection(frameState, fog, lights, prepared, submit
20081
22142
  memoryBytes: 0
20082
22143
  });
20083
22144
  }
20084
- function recordFrame(internals, world, cameras, lights, renderables, transparentDispatch, frameState, dispatchCounts, bindGroupCounts, skylight, skylightCount, skybox, skyboxCount, postProcessParams, worlds = [world], profilePhase, gpuDriven, renderReadLeases, featureGraphCandidate, pointsLinesOwner, cubeCapture, environmentSignature = "", fogSignature = "", environmentReady = true, transmissionDemand, shadowCasterEntityKeys = EMPTY_ENTITY_KEYS, shadowCasterDrawKeys = void 0, volumetricFog, timingCapture, shadowCasterMembership, shadowCasterProjection, shadowCasterWorldKeys, preparedLighting, ssrDependencies, onRenderableDraw) {
22145
+ function recordFrame(internals, world, cameras, lights, renderables, transparentDispatch, frameState, dispatchCounts, bindGroupCounts, skylight, skylightCount, skybox, skyboxCount, postProcessParams, worlds = [world], profilePhase, gpuDriven, renderReadLeases, featureGraphCandidate, pointsLinesOwner, cubeCapture, environmentSignature = "", fogSignature = "", environmentReady = true, transmissionDemand, shadowCasterEntityKeys = EMPTY_ENTITY_KEYS, shadowCasterDrawKeys = void 0, volumetricFog, timingCapture, shadowCasterMembership, shadowCasterProjection, shadowCasterWorldKeys, preparedLighting, ssrDependencies, onRenderableDraw, analyticFog, sampleTimeSeconds, temporalReset = false) {
20085
22146
  frameState.reflectionFallbackObservationSource = void 0;
20086
22147
  frameState.reflectionFallbackCompletion = void 0;
20087
22148
  let activeCameras = cameras;
@@ -20321,6 +22382,7 @@ function recordFrame(internals, world, cameras, lights, renderables, transparent
20321
22382
  ...frameGpuDriven.activeEntityKeys === void 0 ? {} : { activeEntityKeys: frameGpuDriven.activeEntityKeys },
20322
22383
  ...frameGpuDriven.activeEntityRevision === void 0 ? {} : { activeEntityRevision: frameGpuDriven.activeEntityRevision },
20323
22384
  ...frameGpuDriven.telemetryCandidateCount === void 0 ? {} : { telemetryCandidateCount: frameGpuDriven.telemetryCandidateCount },
22385
+ ...frameGpuDriven.telemetrySubmit === void 0 ? {} : { telemetrySubmit: frameGpuDriven.telemetrySubmit },
20324
22386
  ...frameGpuDriven.occlusion === void 0 ? {} : { occlusion: frameGpuDriven.occlusion },
20325
22387
  ...frameGpuDriven.scene?.structureMetrics === void 0 ? {} : {
20326
22388
  structureMetrics: {
@@ -20424,7 +22486,8 @@ function recordFrame(internals, world, cameras, lights, renderables, transparent
20424
22486
  standard,
20425
22487
  void 0,
20426
22488
  ssrAdmission,
20427
- renderExtent
22489
+ renderExtent,
22490
+ analyticFog !== void 0 && analyticFog.density > 0 && analyticFog.maxOpacity > 0
20428
22491
  )
20429
22492
  );
20430
22493
  if (preflightGraph === null) {
@@ -20487,6 +22550,21 @@ function recordFrame(internals, world, cameras, lights, renderables, transparent
20487
22550
  const targetW = swapTarget.targetW;
20488
22551
  const targetH = swapTarget.targetH;
20489
22552
  const previousTemporalView = frameState.lastSuccessfulTemporalView;
22553
+ const motionBlurDemand = camera.motionBlur === void 0 ? false : motionBlurTemporalDemand({
22554
+ shutterAngle: camera.motionBlur.shutterAngle,
22555
+ maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
22556
+ sampleCount: camera.motionBlur.sampleCount,
22557
+ targetFps: camera.motionBlur.targetFps ?? 60
22558
+ });
22559
+ const temporalDemand = camera.antialias === "taa" || motionBlurDemand;
22560
+ const clock = frameClockSample(world, worlds, camera, sampleTimeSeconds);
22561
+ const deltaSeconds = temporalReset || clock.invalidSampleTime ? Number.NaN : motionBlurSampleDelta(
22562
+ clock.elapsedSeconds,
22563
+ previousTemporalView?.sampleTimeSeconds,
22564
+ clock.deltaSeconds
22565
+ );
22566
+ const intervalValid = isMotionBlurIntervalValid(deltaSeconds);
22567
+ const motionBlurFirstFrame = motionBlurDemand && camera.antialias !== "taa" && (previousTemporalView === void 0 || previousTemporalView.mode === "off" && !previousTemporalView.historyValid && previousTemporalView.resetReason === void 0);
20490
22568
  const currentUnjitteredViewProjection = mat4.multiply(
20491
22569
  mat4.create(),
20492
22570
  computeProjectionMatrix(camera),
@@ -20511,16 +22589,20 @@ function recordFrame(internals, world, cameras, lights, renderables, transparent
20511
22589
  internalHeight: renderExtent.internalHeight
20512
22590
  },
20513
22591
  ...camera.historyVersion === void 0 ? {} : { historyVersion: camera.historyVersion },
22592
+ ...clock.elapsedSeconds === void 0 ? {} : { sampleTimeSeconds: clock.elapsedSeconds },
20514
22593
  historyValid: false,
20515
22594
  currentUnjitteredViewProjection,
20516
22595
  currentCameraPosition
20517
22596
  });
20518
- const resetReason = camera.antialias === "taa" && environmentReady ? resolveTemporalReset(previousTemporalView, temporalCandidate) : void 0;
22597
+ const resetReason = temporalDemand && environmentReady ? resolveTemporalReset(previousTemporalView, temporalCandidate) ?? (temporalReset || motionBlurDemand && !intervalValid ? "time-discontinuity" : motionBlurFirstFrame ? "first-frame" : void 0) : void 0;
20519
22598
  const canReusePrevious = previousTemporalView !== void 0 && resetReason === void 0;
20520
22599
  const temporalView = createTemporalView({
20521
22600
  ...temporalCandidate.input,
20522
22601
  frameIndex: resetReason === void 0 ? temporalCandidate.input.frameIndex : 0,
20523
- historyValid: camera.antialias === "taa" && environmentReady && canReusePrevious,
22602
+ // Motion Blur needs a finite positive pair before it can read the
22603
+ // baseline. TAA-only frames retain their existing temporal contract and
22604
+ // do not depend on the simulation clock being present in a test host.
22605
+ historyValid: temporalDemand && environmentReady && canReusePrevious && (!motionBlurDemand || intervalValid),
20524
22606
  resetReason,
20525
22607
  ...canReusePrevious && previousTemporalView.currentUnjitteredViewProjection !== void 0 ? {
20526
22608
  previousUnjitteredViewProjection: previousTemporalView.currentUnjitteredViewProjection
@@ -20559,17 +22641,23 @@ function recordFrame(internals, world, cameras, lights, renderables, transparent
20559
22641
  );
20560
22642
  activeVolumetricFogContext = resolvedHistory.context;
20561
22643
  activeVolumetricFogSignature = resolvedHistory.signature;
20562
- const historyFlag = new Float32Array([activeVolumetricFogContext.historyValid ? 1 : 0]);
22644
+ const previousTime = frameState.volumetricFogHistorySignature?.worldTimeSeconds;
22645
+ const currentTime = resolvedHistory.signature.worldTimeSeconds;
22646
+ const historyParams = new Float32Array([
22647
+ activeVolumetricFogContext.historyValid ? 1 : 0,
22648
+ currentTime ?? 0,
22649
+ previousTime === void 0 || currentTime === void 0 ? 0 : Math.max(0, currentTime - previousTime)
22650
+ ]);
20563
22651
  if (frameState.volumetricFogPendingParams !== void 0) {
20564
- frameState.volumetricFogPendingParams[31] = historyFlag[0] ?? 0;
22652
+ frameState.volumetricFogPendingParams.set(historyParams, 31);
20565
22653
  }
20566
- const historyFlagWritten = internals.device.queue.writeBuffer(
22654
+ const historyParamsWritten = internals.device.queue.writeBuffer(
20567
22655
  activeVolumetricFogContext.paramsBuffer,
20568
22656
  31 * Float32Array.BYTES_PER_ELEMENT,
20569
- historyFlag
22657
+ historyParams
20570
22658
  );
20571
- if (!historyFlagWritten.ok) {
20572
- internals.errorRegistry.fire(historyFlagWritten.error);
22659
+ if (!historyParamsWritten.ok) {
22660
+ internals.errorRegistry.fire(historyParamsWritten.error);
20573
22661
  abortPreparedGpuDriven();
20574
22662
  return false;
20575
22663
  }
@@ -20759,14 +22847,33 @@ function recordFrame(internals, world, cameras, lights, renderables, transparent
20759
22847
  );
20760
22848
  }
20761
22849
  const framePostProcessParams = new Map(postProcessParams);
22850
+ if (analyticFog !== void 0)
22851
+ framePostProcessParams.set(
22852
+ "forgeax.analytic-fog",
22853
+ encodeAnalyticFogParams(analyticFog, recordCamera)
22854
+ );
20762
22855
  if (camera.motionBlur !== void 0) {
20763
- const motionBlurParams = new Uint8Array(16);
20764
- const motionBlurView = new DataView(motionBlurParams.buffer);
20765
- motionBlurView.setFloat32(0, camera.motionBlur.shutterAngle, true);
20766
- motionBlurView.setFloat32(4, camera.motionBlur.maxRadiusPixels, true);
20767
- motionBlurView.setUint32(8, camera.motionBlur.sampleCount, true);
20768
- motionBlurView.setUint32(12, temporalView?.historyValid === true ? 0 : 1, true);
20769
- framePostProcessParams.set("forgeax.motion-blur", motionBlurParams);
22856
+ if (motionBlurDemand) {
22857
+ const motionBlurParams = new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE);
22858
+ const motionBlurView = new DataView(motionBlurParams.buffer);
22859
+ const uniformDeltaSeconds = Number.isFinite(deltaSeconds) ? deltaSeconds : 0;
22860
+ motionBlurView.setFloat32(0, camera.motionBlur.shutterAngle, true);
22861
+ motionBlurView.setFloat32(4, camera.motionBlur.maxRadiusPixels, true);
22862
+ motionBlurView.setUint32(
22863
+ 8,
22864
+ effectiveMotionBlurSampleCount(camera.motionBlur.sampleCount),
22865
+ true
22866
+ );
22867
+ motionBlurView.setUint32(12, temporalView?.historyValid === true ? 0 : 1, true);
22868
+ motionBlurView.setFloat32(16, camera.motionBlur.targetFps ?? 60, true);
22869
+ motionBlurView.setFloat32(
22870
+ 20,
22871
+ motionBlurExposureScale(uniformDeltaSeconds, camera.motionBlur.targetFps ?? 60),
22872
+ true
22873
+ );
22874
+ motionBlurView.setFloat32(24, uniformDeltaSeconds, true);
22875
+ framePostProcessParams.set("forgeax.motion-blur", motionBlurParams);
22876
+ }
20770
22877
  }
20771
22878
  if (camera.depthOfField !== void 0) {
20772
22879
  const dofParams = packDepthOfFieldParams(camera.depthOfField, {
@@ -21939,6 +24046,7 @@ function createTemporalFrameTransaction(options) {
21939
24046
  let staged;
21940
24047
  let historyEpoch = 0;
21941
24048
  let pendingResetReason;
24049
+ let lastFailure;
21942
24050
  return {
21943
24051
  stage(input) {
21944
24052
  staged = copyInput(input);
@@ -21946,9 +24054,11 @@ function createTemporalFrameTransaction(options) {
21946
24054
  commit(submit) {
21947
24055
  if (!submit.accepted) {
21948
24056
  staged = void 0;
24057
+ lastFailure = submit.reason ?? "queue-submit-failed";
21949
24058
  return err$1(new TemporalFrameSubmitError(submit.reason ?? "queue-submit-failed"));
21950
24059
  }
21951
24060
  if (staged === void 0) {
24061
+ lastFailure = "queue-submit-failed";
21952
24062
  return err$1(new TemporalFrameSubmitError("queue-submit-failed"));
21953
24063
  }
21954
24064
  const resetReason = submit.resetReason ?? staged.resetReason ?? pendingResetReason ?? "none";
@@ -21969,6 +24079,7 @@ function createTemporalFrameTransaction(options) {
21969
24079
  accepted = next;
21970
24080
  staged = void 0;
21971
24081
  pendingResetReason = void 0;
24082
+ lastFailure = void 0;
21972
24083
  return ok$1(copyFrame(next));
21973
24084
  },
21974
24085
  snapshot() {
@@ -21976,11 +24087,19 @@ function createTemporalFrameTransaction(options) {
21976
24087
  throw new Error("TemporalFrame snapshot is unavailable before commit");
21977
24088
  return copyFrame(accepted);
21978
24089
  },
24090
+ inspect() {
24091
+ return {
24092
+ accepted: accepted !== void 0,
24093
+ staged: staged !== void 0,
24094
+ ...lastFailure === void 0 ? {} : { lastFailure }
24095
+ };
24096
+ },
21979
24097
  reset(reason2 = "signature-change") {
21980
24098
  accepted = void 0;
21981
24099
  staged = void 0;
21982
24100
  historyEpoch += 1;
21983
24101
  pendingResetReason = reason2;
24102
+ lastFailure = void 0;
21984
24103
  }
21985
24104
  };
21986
24105
  }
@@ -22311,6 +24430,10 @@ function createRenderSystemRecovery(deps) {
22311
24430
  worlds: seed.worlds,
22312
24431
  owner: seed.resourceOwner,
22313
24432
  frameNumber: candidateFrameState.frameNumber,
24433
+ frameSize: {
24434
+ width: runtime.internals.canvas.width,
24435
+ height: runtime.internals.canvas.height
24436
+ },
22314
24437
  visibilitySnapshots: seed.frame.featureVisibilitySnapshots,
22315
24438
  hiddenEntityReports: seed.frame.hiddenEntityReports,
22316
24439
  targets: featureTargets,
@@ -22538,16 +24661,21 @@ function createRenderSystemRecovery(deps) {
22538
24661
  throw new Error("recovery TAA pipeline is unavailable");
22539
24662
  }
22540
24663
  }
22541
- if (camera.motionBlur?.shutterAngle !== void 0 && camera.motionBlur.shutterAngle > 0) {
24664
+ if (camera.motionBlur !== void 0 && motionBlurTemporalDemand({
24665
+ shutterAngle: camera.motionBlur.shutterAngle,
24666
+ maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
24667
+ sampleCount: camera.motionBlur.sampleCount,
24668
+ targetFps: camera.motionBlur.targetFps ?? 60
24669
+ })) {
22542
24670
  warmPostProcessPipeline("forgeax.motion-blur", "rgba16float");
22543
24671
  }
22544
24672
  for (const id of candidatePostProcessIds) {
22545
24673
  warmPostProcessPipeline(id, runtime.pipelineState.format);
22546
24674
  }
22547
- if (camera.bloom === "on" && camera.tonemap !== "none") {
24675
+ if (standardBloomAdmitted(camera)) {
22548
24676
  runtime.pipelineState.perPassResources.ensureBloomResources?.();
22549
24677
  const bloom = runtime.pipelineState.perPassResources.getBloomResources?.();
22550
- if (bloom === null || bloom === void 0 || bloom.bloomBrightPipeline === null || bloom.bloomBlurHPipeline === null || bloom.bloomBlurVPipeline === null || bloom.bloomCompositePipeline === null || bloom.bloomBrightBindGroupLayout === null || bloom.bloomBlurBindGroupLayout === null || bloom.bloomCompositeBindGroupLayout === null || bloom.bloomSampler === null || bloom.bloomBrightParamsBuffer === null || bloom.bloomBlurHParamsBuffer === null || bloom.bloomBlurVParamsBuffer === null || bloom.bloomCompositeParamsBuffer === null) {
24678
+ if (bloom === null || bloom === void 0 || bloom.bloomDownsamplePipeline === null || bloom.bloomUpsamplePipeline === null || bloom.bloomCompositePipeline === null || bloom.bloomDownsampleBindGroupLayout === null || bloom.bloomUpsampleBindGroupLayout === null || bloom.bloomCompositeBindGroupLayout === null || bloom.bloomSampler === null || bloom.bloomDownsampleParamsBuffer === null || bloom.bloomUpsampleParamsBuffer === null || bloom.bloomCompositeParamsBuffer === null) {
22551
24679
  throw new Error("recovery Bloom resources are incomplete");
22552
24680
  }
22553
24681
  }
@@ -24819,6 +26947,30 @@ function coalesceSlots(slots) {
24819
26947
  }
24820
26948
  return ranges;
24821
26949
  }
26950
+ function previousInstanceOrdinals(current, temporal) {
26951
+ const previous = temporal?.previousInstances;
26952
+ const currentGenerations = current?.generations;
26953
+ const previousGenerations = previous?.generations;
26954
+ if (temporal?.motionValid !== true || current === void 0 || previous === void 0 || currentGenerations === void 0 || previousGenerations === void 0 || currentGenerations.length !== current.instanceCount || previousGenerations.length !== previous.instanceCount || current.transforms.length < current.instanceCount * 16 || previous.transforms.length < previous.instanceCount * 16) {
26955
+ return void 0;
26956
+ }
26957
+ const currentSet = /* @__PURE__ */ new Set();
26958
+ for (const generation of currentGenerations) {
26959
+ if (generation === 0 || currentSet.has(generation)) return void 0;
26960
+ currentSet.add(generation);
26961
+ }
26962
+ const ordinals = /* @__PURE__ */ new Map();
26963
+ for (let ordinal = 0; ordinal < previousGenerations.length; ordinal += 1) {
26964
+ const generation = previousGenerations[ordinal] ?? 0;
26965
+ if (generation === 0 || ordinals.has(generation)) return void 0;
26966
+ ordinals.set(generation, ordinal);
26967
+ }
26968
+ return ordinals;
26969
+ }
26970
+ function instanceIdentityChanged(resident, current, ordinal) {
26971
+ if (resident === void 0 || current === void 0) return resident !== current;
26972
+ return resident[ordinal] !== current[ordinal];
26973
+ }
24822
26974
  var GpuScene = class _GpuScene {
24823
26975
  constructor(device, capacity, buffers) {
24824
26976
  this.device = device;
@@ -24849,6 +27001,8 @@ var GpuScene = class _GpuScene {
24849
27001
  sceneContentRevision = 0;
24850
27002
  identityUploaded = false;
24851
27003
  allocations = /* @__PURE__ */ new Map();
27004
+ /** Current stable instance identities retained beside compact GPU rows. */
27005
+ instanceGenerations = /* @__PURE__ */ new Map();
24852
27006
  instances = new StableRangeAllocator();
24853
27007
  transforms = new StableRangeAllocator();
24854
27008
  draws = new StableRangeAllocator();
@@ -24904,7 +27058,7 @@ var GpuScene = class _GpuScene {
24904
27058
  }
24905
27059
  return allocation.materialStart + materialSlot;
24906
27060
  }
24907
- sync(delta) {
27061
+ sync(delta, temporalResolver) {
24908
27062
  for (const transformIndex of this.pendingTemporalUploads) {
24909
27063
  this.writesByTable.transform.push(transformIndex);
24910
27064
  }
@@ -24962,11 +27116,11 @@ var GpuScene = class _GpuScene {
24962
27116
  for (const record of publications) {
24963
27117
  const reset = newSlots.has(record.slot) || allocationResets.has(record.slot);
24964
27118
  if (reset || contentUpdatedSlotIds.has(record.slot)) {
24965
- this.writeSlot(record, reset, writesByTable);
27119
+ this.writeSlot(record, reset, writesByTable, temporalResolver?.(record));
24966
27120
  continue;
24967
27121
  }
24968
27122
  if (instanceUpdatedSlotIds.has(record.slot))
24969
- this.writeInstanceTransforms(record, writesByTable);
27123
+ this.writeInstanceTransforms(record, writesByTable, temporalResolver?.(record));
24970
27124
  this.writeRootTransform(record, false, writesByTable);
24971
27125
  }
24972
27126
  const uploaded = this.uploadRows(writesByTable);
@@ -25026,6 +27180,7 @@ var GpuScene = class _GpuScene {
25026
27180
  }
25027
27181
  rebuild(slots) {
25028
27182
  this.allocations.clear();
27183
+ this.instanceGenerations.clear();
25029
27184
  this.instances.reset();
25030
27185
  this.transforms.reset(1);
25031
27186
  this.draws.reset();
@@ -25111,7 +27266,7 @@ var GpuScene = class _GpuScene {
25111
27266
  for (const buffer of this.retiredBuffers) this.device.destroyBuffer(buffer);
25112
27267
  for (const buffer of Object.values(this.buffers)) this.device.destroyBuffer(buffer);
25113
27268
  }
25114
- writeSlot(record, resetPrevious, writes) {
27269
+ writeSlot(record, resetPrevious, writes, temporal) {
25115
27270
  const allocation = this.allocations.get(record.slot);
25116
27271
  if (allocation === void 0) throw new RangeError("GPU Scene allocation unavailable");
25117
27272
  const primitiveOffset = record.slot * PRIMITIVE.stride;
@@ -25167,6 +27322,11 @@ var GpuScene = class _GpuScene {
25167
27322
  ]);
25168
27323
  const instance = new DataView(this.tableBytes.instance);
25169
27324
  const transform = this.transformView;
27325
+ const instances = record.snapshot.instances;
27326
+ const previousInstances = temporal?.previousInstances;
27327
+ const previousOrdinals = previousInstanceOrdinals(instances, temporal);
27328
+ const currentGenerations = instances?.generations;
27329
+ const residentGenerations = this.instanceGenerations.get(record.slot);
25170
27330
  for (let ordinal = 0; ordinal < allocation.instanceCount; ordinal += 1) {
25171
27331
  const instanceIndex = allocation.instanceStart + ordinal;
25172
27332
  const instanceOffset = instanceIndex * INSTANCE.stride;
@@ -25188,7 +27348,7 @@ var GpuScene = class _GpuScene {
25188
27348
  ) || metadataChanged;
25189
27349
  metadataChanged = writeChangedU32(instance, instanceOffset + offset(INSTANCE, "flags"), PRIMITIVE_ACTIVE) || metadataChanged;
25190
27350
  if (allocation.instanceTransformCount > 0) {
25191
- const transforms = record.snapshot.instances?.transforms;
27351
+ const transforms = instances?.transforms;
25192
27352
  const hasLocal = transforms !== void 0 && transforms.length >= (ordinal + 1) * 16;
25193
27353
  const local = hasLocal ? transforms : IDENTITY_MATRIX;
25194
27354
  const sourceOffset = hasLocal ? ordinal * 16 : 0;
@@ -25199,18 +27359,55 @@ var GpuScene = class _GpuScene {
25199
27359
  local,
25200
27360
  sourceOffset
25201
27361
  );
25202
- if (resetPrevious)
25203
- writeChangedMat4(
25204
- transform,
25205
- localOffset + offset(TRANSFORM, "previousWorld"),
25206
- local,
25207
- sourceOffset
25208
- );
25209
- else if (changed) this.pendingTemporalTransforms.add(transformIndex);
25210
- if (changed || resetPrevious) writes.transform.push(transformIndex);
27362
+ const identityChanged = instanceIdentityChanged(
27363
+ residentGenerations,
27364
+ currentGenerations,
27365
+ ordinal
27366
+ );
27367
+ if (resetPrevious) {
27368
+ const previousOrdinal = previousOrdinals?.get(currentGenerations?.[ordinal] ?? 0);
27369
+ const previousSource = previousOrdinal === void 0 || previousInstances === void 0 ? void 0 : previousInstances.transforms;
27370
+ if (previousSource === void 0 || previousOrdinal === void 0) {
27371
+ writeChangedMat4(
27372
+ transform,
27373
+ localOffset + offset(TRANSFORM, "previousWorld"),
27374
+ local,
27375
+ sourceOffset
27376
+ );
27377
+ } else {
27378
+ writeChangedMat4(
27379
+ transform,
27380
+ localOffset + offset(TRANSFORM, "previousWorld"),
27381
+ previousSource,
27382
+ previousOrdinal * 16
27383
+ );
27384
+ this.pendingTemporalTransforms.add(transformIndex);
27385
+ }
27386
+ } else if (changed || identityChanged) {
27387
+ const previousOrdinal = previousOrdinals?.get(currentGenerations?.[ordinal] ?? 0);
27388
+ const previousSource = previousOrdinal === void 0 || previousInstances === void 0 ? void 0 : previousInstances.transforms;
27389
+ if (previousSource === void 0 || previousOrdinal === void 0) {
27390
+ writeChangedMat4(
27391
+ transform,
27392
+ localOffset + offset(TRANSFORM, "previousWorld"),
27393
+ local,
27394
+ sourceOffset
27395
+ );
27396
+ } else {
27397
+ writeChangedMat4(
27398
+ transform,
27399
+ localOffset + offset(TRANSFORM, "previousWorld"),
27400
+ previousSource,
27401
+ previousOrdinal * 16
27402
+ );
27403
+ }
27404
+ this.pendingTemporalTransforms.add(transformIndex);
27405
+ }
27406
+ if (changed || identityChanged || resetPrevious) writes.transform.push(transformIndex);
25211
27407
  }
25212
27408
  if (metadataChanged || resetPrevious) writes.instance.push(instanceIndex);
25213
27409
  }
27410
+ this.rememberInstanceGenerations(record.slot, currentGenerations, allocation.instanceCount);
25214
27411
  this.writeRootTransform(record, resetPrevious, writes);
25215
27412
  writes.primitive.push(record.slot);
25216
27413
  const draw = new DataView(this.tableBytes.drawTemplate);
@@ -25270,7 +27467,7 @@ var GpuScene = class _GpuScene {
25270
27467
  }
25271
27468
  }
25272
27469
  /** Consume changed instance fields independently of root or material changes. */
25273
- writeInstanceTransforms(record, writes) {
27470
+ writeInstanceTransforms(record, writes, temporal) {
25274
27471
  const allocation = this.allocations.get(record.slot);
25275
27472
  const instances = record.snapshot.instances;
25276
27473
  if (allocation === void 0 || instances === void 0) {
@@ -25279,19 +27476,62 @@ var GpuScene = class _GpuScene {
25279
27476
  if (allocation.instanceTransformCount !== instances.instanceCount) {
25280
27477
  throw new RangeError("GPU Scene instance update changed instance count");
25281
27478
  }
27479
+ const previousOrdinals = previousInstanceOrdinals(instances, temporal);
27480
+ const previousInstances = temporal?.previousInstances;
27481
+ const currentGenerations = instances.generations;
27482
+ const residentGenerations = this.instanceGenerations.get(record.slot);
25282
27483
  for (let ordinal = 0; ordinal < allocation.instanceTransformCount; ordinal += 1) {
25283
27484
  const transformIndex = allocation.instanceTransformStart + ordinal;
25284
27485
  const transformOffset = transformIndex * TRANSFORM.stride;
25285
- if (writeChangedMat4(
27486
+ const changed = writeChangedMat4(
25286
27487
  this.transformView,
25287
27488
  transformOffset + offset(TRANSFORM, "currentWorld"),
25288
27489
  instances.transforms,
25289
27490
  ordinal * 16
25290
- )) {
27491
+ );
27492
+ const identityChanged = instanceIdentityChanged(
27493
+ residentGenerations,
27494
+ currentGenerations,
27495
+ ordinal
27496
+ );
27497
+ if (changed || identityChanged) {
27498
+ const previousOrdinal = previousOrdinals?.get(currentGenerations?.[ordinal] ?? 0);
27499
+ const previousSource = previousOrdinal === void 0 || previousInstances === void 0 ? void 0 : previousInstances.transforms;
27500
+ if (previousSource === void 0 || previousOrdinal === void 0) {
27501
+ writeChangedMat4(
27502
+ this.transformView,
27503
+ transformOffset + offset(TRANSFORM, "previousWorld"),
27504
+ instances.transforms,
27505
+ ordinal * 16
27506
+ );
27507
+ } else {
27508
+ writeChangedMat4(
27509
+ this.transformView,
27510
+ transformOffset + offset(TRANSFORM, "previousWorld"),
27511
+ previousSource,
27512
+ previousOrdinal * 16
27513
+ );
27514
+ }
25291
27515
  this.pendingTemporalTransforms.add(transformIndex);
25292
27516
  writes.transform.push(transformIndex);
25293
27517
  }
25294
27518
  }
27519
+ this.rememberInstanceGenerations(
27520
+ record.slot,
27521
+ currentGenerations,
27522
+ allocation.instanceTransformCount
27523
+ );
27524
+ }
27525
+ rememberInstanceGenerations(slot, generations2, instanceCount) {
27526
+ if (generations2 === void 0 || generations2.length !== instanceCount) {
27527
+ this.instanceGenerations.delete(slot);
27528
+ return;
27529
+ }
27530
+ const resident = this.instanceGenerations.get(slot);
27531
+ if (resident !== void 0 && resident.length === generations2.length && resident.every((generation, ordinal) => generation === generations2[ordinal])) {
27532
+ return;
27533
+ }
27534
+ this.instanceGenerations.set(slot, generations2.slice());
25295
27535
  }
25296
27536
  writeRootTransform(record, resetPrevious, writes) {
25297
27537
  const allocation = this.allocations.get(record.slot);
@@ -25317,6 +27557,7 @@ var GpuScene = class _GpuScene {
25317
27557
  }
25318
27558
  const allocation = this.allocations.get(slot);
25319
27559
  if (allocation === void 0) return;
27560
+ this.instanceGenerations.delete(slot);
25320
27561
  this.clearAllocation(allocation, writes);
25321
27562
  this.allocations.delete(slot);
25322
27563
  }
@@ -25798,6 +28039,25 @@ var VisibilityFacetStore = class {
25798
28039
  };
25799
28040
 
25800
28041
  // src/scene/render-scene.ts
28042
+ function sameInstanceIdentitySet(current, previous) {
28043
+ if (current === void 0 || previous === void 0) return false;
28044
+ const currentSet = new Set(current);
28045
+ const previousSet = new Set(previous);
28046
+ if (currentSet.size !== current.length || previousSet.size !== previous.length || current.some((identity) => identity === 0) || previous.some((identity) => identity === 0)) {
28047
+ return false;
28048
+ }
28049
+ if (current.length === previous.length) {
28050
+ let sameOrder = true;
28051
+ for (let index = 0; index < current.length; index += 1) {
28052
+ if (current[index] !== previous[index]) {
28053
+ sameOrder = false;
28054
+ break;
28055
+ }
28056
+ }
28057
+ if (sameOrder) return true;
28058
+ }
28059
+ return current.every((identity) => previousSet.has(identity));
28060
+ }
25801
28061
  function identityKey(worldId, entityKey) {
25802
28062
  return `${worldId}:${entityKey}`;
25803
28063
  }
@@ -25837,7 +28097,8 @@ function ownSnapshot(snapshot) {
25837
28097
  ...snapshot.instances === void 0 ? {} : {
25838
28098
  instances: {
25839
28099
  ...snapshot.instances,
25840
- transforms: new Float32Array(snapshot.instances.transforms)
28100
+ transforms: new Float32Array(snapshot.instances.transforms),
28101
+ ...snapshot.instances.generations === void 0 ? {} : { generations: new Uint32Array(snapshot.instances.generations) }
25841
28102
  }
25842
28103
  },
25843
28104
  ...snapshot.spriteInstances === void 0 ? {} : {
@@ -26002,7 +28263,8 @@ var RenderScene = class {
26002
28263
  ...pending.current,
26003
28264
  instances: {
26004
28265
  ...operation.instances,
26005
- transforms: new Float32Array(operation.instances.transforms)
28266
+ transforms: new Float32Array(operation.instances.transforms),
28267
+ ...operation.instances.generations === void 0 ? {} : { generations: new Uint32Array(operation.instances.generations) }
26006
28268
  }
26007
28269
  };
26008
28270
  pending.instanceChanged = true;
@@ -26424,13 +28686,23 @@ var RenderScene = class {
26424
28686
  const seedCurrent = !sameGeneration || !wasVisible || reasons.includes("geometry-revision") || reasons.includes("material-revision");
26425
28687
  const previous = seedCurrent || submitted === void 0 ? slot.snapshot : submitted.snapshot;
26426
28688
  const previousWorld = this.previousWorldBySlot[slot.slot];
28689
+ const currentInstances = slot.snapshot.instances;
28690
+ const submittedInstances = submitted?.snapshot.instances;
28691
+ const instancesCompatible = currentInstances === void 0 && submittedInstances === void 0 ? true : currentInstances !== void 0 && submittedInstances !== void 0 && (currentInstances.collectionId === void 0 || submittedInstances.collectionId === void 0 || currentInstances.collectionId === submittedInstances.collectionId) && currentInstances.transforms.length >= currentInstances.instanceCount * 16 && submittedInstances.transforms.length >= submittedInstances.instanceCount * 16 && sameInstanceIdentitySet(currentInstances.generations, submittedInstances.generations);
28692
+ const motionValid = !seedCurrent && previousWorld !== void 0 && instancesCompatible;
26427
28693
  return {
26428
28694
  previousEpoch: seedCurrent ? void 0 : this.submittedEpoch,
26429
28695
  previousSource: seedCurrent ? "current-seed" : "last-submitted",
28696
+ motionValid,
26430
28697
  reactive: reasons.length > 0,
26431
28698
  reactiveReasons: reasons,
26432
28699
  previousTransform: seedCurrent || previousWorld === void 0 ? previous.transform : { ...previous.transform, world: previousWorld },
26433
- previousInstances: previous.instances,
28700
+ // A collection identity/count break is a temporal cut for that
28701
+ // renderable. The record stage receives the current snapshot as its
28702
+ // baseline, while stable collections retain the submitted column and
28703
+ // use identity generations to map reordered entries and seed replaced
28704
+ // entries.
28705
+ previousInstances: seedCurrent ? slot.snapshot.instances : previous.instances,
26434
28706
  previousSkin: previous.skin,
26435
28707
  previousMorphWeights: previous.morph?.weights
26436
28708
  };
@@ -26618,7 +28890,13 @@ function topologyDeltaForAvailable(delta, unavailableSlots, newlyUnavailable) {
26618
28890
  }
26619
28891
  function frameDemandsTemporal(frame) {
26620
28892
  const camera = frame.cameras[0];
26621
- return camera?.antialias === "taa" || (camera?.motionBlur?.shutterAngle ?? 0) > 0;
28893
+ const motionBlur = camera?.motionBlur;
28894
+ return camera?.antialias === "taa" || motionBlur !== void 0 && motionBlurTemporalDemand({
28895
+ shutterAngle: motionBlur.shutterAngle,
28896
+ maxRadiusPixels: motionBlur.maxRadiusPixels,
28897
+ sampleCount: motionBlur.sampleCount,
28898
+ targetFps: motionBlur.targetFps ?? 60
28899
+ });
26622
28900
  }
26623
28901
  function cameraFrusta(cameras) {
26624
28902
  const planes = [];
@@ -27625,12 +29903,16 @@ var PersistentRenderScene = class {
27625
29903
  /** Publish renderer-owned previous transforms after queue submission. */
27626
29904
  commitTemporalFrame() {
27627
29905
  const gpuCommit = this.gpuScene?.commitTemporalFrame(this.temporalCapture !== void 0);
27628
- if (gpuCommit !== void 0 && !gpuCommit.ok) return gpuCommit;
29906
+ if (gpuCommit !== void 0 && !gpuCommit.ok) {
29907
+ this.temporalCapture = void 0;
29908
+ return gpuCommit;
29909
+ }
27629
29910
  const temporalCapture = this.temporalCapture;
27630
29911
  if (temporalCapture !== void 0) {
27631
29912
  try {
27632
29913
  temporalCapture.projection.commitSubmission(temporalCapture.capture);
27633
29914
  } catch (cause) {
29915
+ this.temporalCapture = void 0;
27634
29916
  return err$1(
27635
29917
  new RhiError({
27636
29918
  code: "internal-error",
@@ -27643,6 +29925,10 @@ var PersistentRenderScene = class {
27643
29925
  }
27644
29926
  return ok$1(void 0);
27645
29927
  }
29928
+ /** Drop a staged temporal capture when the owning frame did not submit. */
29929
+ discardTemporalFrame() {
29930
+ this.temporalCapture = void 0;
29931
+ }
27646
29932
  /** Capture current CPU scene facts; commitTemporalFrame publishes them only after submit. */
27647
29933
  prepareTemporalFrame(visible) {
27648
29934
  const projection = this.composition?.projection;
@@ -27934,7 +30220,7 @@ var PersistentRenderScene = class {
27934
30220
  removedSlots: [],
27935
30221
  recreatedSlots: [],
27936
30222
  resynced: 0
27937
- }) : acquired.scene.sync(delta);
30223
+ }) : acquired.scene.sync(delta, (slot) => projection.temporalSnapshotBySlot(slot.slot));
27938
30224
  if (!result.ok) {
27939
30225
  this.sceneTableUploadBytes = 0;
27940
30226
  this.failGpu(result.error);
@@ -28344,9 +30630,6 @@ function reportPreparedGraphicsCompletionError(internals, error) {
28344
30630
  })
28345
30631
  );
28346
30632
  }
28347
- function isPendingRenderFeaturePreparation(error) {
28348
- return error.code === "render-feature-preparation-failed" && error.detail.reason.startsWith("rhi-not-available:");
28349
- }
28350
30633
  function makePreparedPipelinePendingError() {
28351
30634
  return new RhiError({
28352
30635
  code: "rhi-not-available",
@@ -28471,6 +30754,19 @@ function createRenderSystem(internals) {
28471
30754
  let preparedFeatureSkylightResources;
28472
30755
  const dynamicGeometryFrames = createDynamicGeometryFrameBindings();
28473
30756
  let latestCamera;
30757
+ let lastMotionBlurInvalidParams;
30758
+ let motionBlurFeatureInput;
30759
+ const motionBlurFeature = createMotionBlurFeature(() => motionBlurFeatureInput);
30760
+ const ensureMotionBlurFeature = () => {
30761
+ const host = internals.featureHost;
30762
+ if (host === void 0 || host.features.some((feature) => feature.identity === "forgeax.motion-blur")) {
30763
+ return;
30764
+ }
30765
+ const installed = host.install(
30766
+ motionBlurFeature
30767
+ );
30768
+ if (!installed.ok) internals.errorRegistry.fire(installed.error);
30769
+ };
28474
30770
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
28475
30771
  const instanceCollections = new InstanceCollectionStore();
28476
30772
  const reflectionProbeOwner = new ReflectionProbeRecordOwner(internals);
@@ -28818,6 +31114,7 @@ function createRenderSystem(internals) {
28818
31114
  // tiles until the first frame with a castShadow spot (AC-03).
28819
31115
  spotShadowSnapshots: []
28820
31116
  };
31117
+ const pendingFeatureErrorLastReportedFrame = /* @__PURE__ */ new Map();
28821
31118
  const resolveSsrDependencies = (requested) => {
28822
31119
  const owner = reflectionProbeOwner.inspect();
28823
31120
  const temporal = frameState.lastSuccessfulTemporalView === void 0 ? void 0 : {
@@ -29162,16 +31459,16 @@ function createRenderSystem(internals) {
29162
31459
  let lodCandidateCountCache;
29163
31460
  let lodInspectionRenderablesCache;
29164
31461
  let orderedDispatchCache;
29165
- const applyLodSelectionTelemetry = (selection) => {
29166
- if (selection === void 0 || lastLodOcclusionInspection === void 0) return;
29167
- const inspection = lastLodOcclusionInspection;
31462
+ const applyLodSelectionTelemetry = (selection, targetInspection = lastLodOcclusionInspection, targetWorldAttachments = lastLodWorldAttachments) => {
31463
+ if (selection === void 0 || targetInspection === void 0) return;
31464
+ const inspection = targetInspection;
29168
31465
  let worlds = inspection.worlds;
29169
31466
  const worldSelections = selection.worldSelections;
29170
31467
  if (worldSelections !== void 0 && worldSelections.length > 0) {
29171
31468
  const selectionsByAttachment = /* @__PURE__ */ new Map();
29172
31469
  let attributionValid = true;
29173
31470
  for (const selected of worldSelections) {
29174
- const attachmentId = lastLodWorldAttachments.get(selected.worldKey);
31471
+ const attachmentId = targetWorldAttachments.get(selected.worldKey);
29175
31472
  if (attachmentId === void 0) {
29176
31473
  attributionValid = false;
29177
31474
  break;
@@ -29184,15 +31481,7 @@ function createRenderSystem(internals) {
29184
31481
  const nextWorlds = worlds.map((world) => {
29185
31482
  const selected = selectionsByAttachment.get(world.attachmentId);
29186
31483
  const baseRow = world.rows[0];
29187
- if (selected === void 0 || baseRow === void 0) {
29188
- if (world.rows.length > 0) attributionValid = false;
29189
- return world;
29190
- }
29191
- const candidateCount = selected.reduce((sum, row) => sum + row.candidateCount, 0);
29192
- if (candidateCount !== baseRow.count.candidates) {
29193
- attributionValid = false;
29194
- return world;
29195
- }
31484
+ if (selected === void 0 || baseRow === void 0) return world;
29196
31485
  return Object.freeze({
29197
31486
  ...world,
29198
31487
  attribution: Object.freeze({
@@ -29222,7 +31511,7 @@ function createRenderSystem(internals) {
29222
31511
  lastLodOcclusionInspection = inspectLodOcclusion({
29223
31512
  ...inspection,
29224
31513
  count: {
29225
- ...lastLodOcclusionInspection.count,
31514
+ ...inspection.count,
29226
31515
  candidates: selection.candidateCount,
29227
31516
  visible: selection.visible,
29228
31517
  occluded: selection.occluded
@@ -29562,22 +31851,20 @@ function createRenderSystem(internals) {
29562
31851
  });
29563
31852
  },
29564
31853
  get motionBlurInspection() {
29565
- const camera = latestCamera;
29566
- if (camera?.motionBlur === void 0) return void 0;
29567
- const demanded = camera.motionBlur.shutterAngle > 0;
29568
- const unavailable3 = demanded && !internals.device.caps.rgba16floatRenderable;
29569
- const admitted = frameState.activeTemporalGpuState !== void 0 || frameState.temporalGpuState !== void 0;
29570
- return {
29571
- enabled: demanded,
29572
- status: !demanded ? "off" : unavailable3 || !admitted ? "reset" : "active",
29573
- shutterAngle: camera.motionBlur.shutterAngle,
29574
- maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
29575
- sampleCount: camera.motionBlur.sampleCount,
29576
- temporalDemand: demanded ? "scene-data-temporal-v1" : "none",
29577
- passName: "motion-blur",
29578
- historyWrites: 0,
29579
- ...unavailable3 ? { lastFailure: "scene-data-unavailable" } : {}
29580
- };
31854
+ return projectMotionBlurInspection({
31855
+ camera: latestCamera,
31856
+ invalidParams: lastMotionBlurInvalidParams,
31857
+ capabilities: internals.device.caps,
31858
+ acceptedTemporal: frameState.lastSuccessfulTemporalView,
31859
+ transaction: frameState.temporalFrameTransaction.inspect(),
31860
+ execution: frameState.motionBlurExecution,
31861
+ temporalFrameId: frameState.temporalFrame?.frameId,
31862
+ deviceGeneration: internals.deviceScope.generation,
31863
+ graphGeneration: frameState.graphGeneration
31864
+ });
31865
+ },
31866
+ get motionBlurInvalidParams() {
31867
+ return lastMotionBlurInvalidParams;
29581
31868
  },
29582
31869
  get depthOfFieldInspection() {
29583
31870
  const camera = latestCamera;
@@ -29635,22 +31922,7 @@ function createRenderSystem(internals) {
29635
31922
  const lifecycle = resources?.inspectBloomResources?.(
29636
31923
  frameState.compiledFrameGraph?.inspect()
29637
31924
  );
29638
- return Object.freeze(
29639
- lifecycle ?? {
29640
- graphStatus: "empty",
29641
- enabled: false,
29642
- targetCount: 0,
29643
- targetBytes: 0,
29644
- resourceCount: 0,
29645
- passCount: 0,
29646
- encodeCount: 0,
29647
- bindGroupCount: 0,
29648
- uploadCount: 0,
29649
- residentChildBytes: 0,
29650
- generation: 0,
29651
- state: "off"
29652
- }
29653
- );
31925
+ return Object.freeze(lifecycle ?? emptyBloomInspection());
29654
31926
  },
29655
31927
  get transmission() {
29656
31928
  return lastTransmissionInspection;
@@ -29714,6 +31986,7 @@ function createRenderSystem(internals) {
29714
31986
  const profileSession = internals.profiler?.activeSession();
29715
31987
  let ownsProfileFrame = false;
29716
31988
  let submitted = false;
31989
+ persistentRenderScene.discardTemporalFrame();
29717
31990
  dynamicGeometryFrames.begin();
29718
31991
  if (profileSession !== void 0 && opts.profileFrame === void 0) {
29719
31992
  try {
@@ -29733,44 +32006,56 @@ function createRenderSystem(internals) {
29733
32006
  }
29734
32007
  dispatchCounts.unlit = 0;
29735
32008
  const resourceWorld = compositionWorlds[resourceOwner];
29736
- const frame = runProfiledRenderPhase(profileSession, "extract", () => {
29737
- return persistentRenderScene.extractComposition(
29738
- compositionWorlds,
29739
- { cameraOwner, resourceOwner },
29740
- internals.assets.catalogEpoch,
29741
- (renderables2) => extractFrames(
32009
+ let frame;
32010
+ try {
32011
+ frame = runProfiledRenderPhase(profileSession, "extract", () => {
32012
+ return persistentRenderScene.extractComposition(
29742
32013
  compositionWorlds,
29743
- {
29744
- cameraOwner,
29745
- resourceOwner,
29746
- ...cameraEntityKey === void 0 ? {} : { cameraEntityKey }
29747
- },
29748
- internals.assets,
29749
- internals.getPipelineState(),
29750
- persistentRenderScene.materialSnapshotCacheStore(),
29751
- {
29752
- ...internals.device.caps.backendKind === "null" ? {} : {
29753
- materialContext: {
29754
- backend: internals.device.caps.backendKind === "wgpu-webgl2" ? "webgl2" : internals.device.caps.backendKind,
29755
- capability: internals.device.caps.storageBuffer ? "storage-buffer" : "uniform-fallback",
29756
- pipeline: "forward",
29757
- geometry: "mesh",
29758
- pass: "forward",
29759
- profile: "forgeax-material-wgsl-v1",
29760
- toolchain: "naga-oil",
29761
- instrumentation: "none"
29762
- }
32014
+ { cameraOwner, resourceOwner },
32015
+ internals.assets.catalogEpoch,
32016
+ (renderables2) => extractFrames(
32017
+ compositionWorlds,
32018
+ {
32019
+ cameraOwner,
32020
+ resourceOwner,
32021
+ ...cameraEntityKey === void 0 ? {} : { cameraEntityKey }
29763
32022
  },
29764
- cull: "none",
29765
- retainHidden: true,
29766
- renderables: renderables2,
29767
- instanceCollections,
29768
- ...!internals.device.caps.storageBuffer || internals.getMaterialShaderArtifact === void 0 ? {} : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }
29769
- }
29770
- ),
29771
- compositionLeases
29772
- );
29773
- });
32023
+ internals.assets,
32024
+ internals.getPipelineState(),
32025
+ persistentRenderScene.materialSnapshotCacheStore(),
32026
+ {
32027
+ ...internals.device.caps.backendKind === "null" ? {} : {
32028
+ materialContext: {
32029
+ backend: internals.device.caps.backendKind === "wgpu-webgl2" ? "webgl2" : internals.device.caps.backendKind,
32030
+ capability: internals.device.caps.storageBuffer ? "storage-buffer" : "uniform-fallback",
32031
+ pipeline: "forward",
32032
+ geometry: "mesh",
32033
+ pass: "forward",
32034
+ profile: "forgeax-material-wgsl-v1",
32035
+ toolchain: "naga-oil",
32036
+ instrumentation: "none"
32037
+ }
32038
+ },
32039
+ cull: "none",
32040
+ retainHidden: true,
32041
+ renderables: renderables2,
32042
+ instanceCollections,
32043
+ ...!internals.device.caps.storageBuffer || internals.getMaterialShaderArtifact === void 0 ? {} : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }
32044
+ }
32045
+ ),
32046
+ compositionLeases
32047
+ );
32048
+ });
32049
+ } catch (cause) {
32050
+ if (cause instanceof MotionBlurValidationError) {
32051
+ lastMotionBlurInvalidParams = cause;
32052
+ latestCamera = void 0;
32053
+ internals.errorRegistry.fire(cause);
32054
+ return false;
32055
+ }
32056
+ throw cause;
32057
+ }
32058
+ lastMotionBlurInvalidParams = void 0;
29774
32059
  const {
29775
32060
  cameras: extractedCameras,
29776
32061
  cubeCameras,
@@ -29976,6 +32261,7 @@ function createRenderSystem(internals) {
29976
32261
  const submissionRenderables = visibilityProjection.renderables;
29977
32262
  const submissionDispatch = visibilityProjection.dispatch;
29978
32263
  const inspectionCamera = latestCamera;
32264
+ const lodSubmitFrameId = internals.observationFrameId ?? frameState.frameNumber;
29979
32265
  let lodCandidateCountForFrame = 0;
29980
32266
  if (inspectionCamera !== void 0) {
29981
32267
  const firstRenderable = renderables[0];
@@ -30007,7 +32293,7 @@ function createRenderSystem(internals) {
30007
32293
  renderables,
30008
32294
  submissionRenderables,
30009
32295
  inspectionCamera,
30010
- frameState.frameNumber,
32296
+ lodSubmitFrameId,
30011
32297
  worldKeys,
30012
32298
  slotByEntity,
30013
32299
  {
@@ -30032,7 +32318,7 @@ function createRenderSystem(internals) {
30032
32318
  lastLodOcclusionInspection = inspectLodOcclusion({
30033
32319
  root: {
30034
32320
  guid: firstRenderable === void 0 ? "none" : `asset-handle:${firstRenderable.assetHandle}`,
30035
- sourceKey: `render-frame:${frameState.frameNumber}`
32321
+ sourceKey: `render-frame:${lodSubmitFrameId}`
30036
32322
  },
30037
32323
  view: {
30038
32324
  attachmentId: compositionWorlds[inspectionCamera.worldId ?? cameraOwner]?.identity ?? "none",
@@ -30044,7 +32330,7 @@ function createRenderSystem(internals) {
30044
32330
  primitiveSlot: 0,
30045
32331
  slotGeneration: persistentRenderScene.inspect().topology.revision
30046
32332
  },
30047
- generation: frameState.frameNumber,
32333
+ generation: lodSubmitFrameId,
30048
32334
  count: {
30049
32335
  candidates: candidateCount,
30050
32336
  visible: inspectionRenderables.length,
@@ -30061,7 +32347,7 @@ function createRenderSystem(internals) {
30061
32347
  visible: true
30062
32348
  })),
30063
32349
  submit: {
30064
- frameId: frameState.frameNumber,
32350
+ frameId: lodSubmitFrameId,
30065
32351
  build: resolveInspectionBuild(internals),
30066
32352
  deviceGeneration: internals.deviceScope.generation
30067
32353
  },
@@ -30072,7 +32358,12 @@ function createRenderSystem(internals) {
30072
32358
  lastLodOcclusionInspection = void 0;
30073
32359
  lastLodWorldAttachments = /* @__PURE__ */ new Map();
30074
32360
  }
30075
- const temporalDemanded = cameras[0]?.antialias === "taa" || (cameras[0]?.motionBlur?.shutterAngle ?? 0) > 0;
32361
+ const temporalDemanded = cameras[0]?.antialias === "taa" || cameras[0]?.motionBlur !== void 0 && motionBlurTemporalDemand({
32362
+ shutterAngle: cameras[0].motionBlur.shutterAngle,
32363
+ maxRadiusPixels: cameras[0].motionBlur.maxRadiusPixels,
32364
+ sampleCount: cameras[0].motionBlur.sampleCount,
32365
+ targetFps: cameras[0].motionBlur.targetFps ?? 60
32366
+ });
30076
32367
  if (temporalDemanded) {
30077
32368
  persistentRenderScene.prepareTemporalFrame(submissionRenderables);
30078
32369
  }
@@ -30163,6 +32454,38 @@ function createRenderSystem(internals) {
30163
32454
  storageBuffer: internals.device.caps.storageBuffer,
30164
32455
  multisample: internals.device.caps.backendKind !== "wgpu-webgl2"
30165
32456
  });
32457
+ const motionBlurParams = cameras[0]?.motionBlur;
32458
+ let motionBlurDelta = 0;
32459
+ try {
32460
+ const delta = compositionWorlds[cameraOwner].getResource(Time).delta;
32461
+ motionBlurDelta = Number.isFinite(delta) ? Math.max(0, delta) : 0;
32462
+ } catch {
32463
+ motionBlurDelta = 0;
32464
+ }
32465
+ const acceptedSampleTime = frameState.lastSuccessfulTemporalView?.sampleTimeSeconds;
32466
+ const cameraViewIdentity = `camera:${cameras[0]?.entityKey ?? 0}`;
32467
+ const canvasWidth = Math.max(1, internals.canvas.width);
32468
+ const canvasHeight = Math.max(1, internals.canvas.height);
32469
+ const motionBlurPlan = planMotionBlurFrame({
32470
+ params: motionBlurParams,
32471
+ sampleTimeSeconds: opts.sampleTimeSeconds,
32472
+ acceptedSampleTimeSeconds: acceptedSampleTime,
32473
+ fallbackDeltaSeconds: motionBlurDelta,
32474
+ acceptedTemporal: frameState.lastSuccessfulTemporalView,
32475
+ temporalReset: opts.temporalReset === true,
32476
+ width: canvasWidth,
32477
+ height: canvasHeight,
32478
+ cameraViewIdentity,
32479
+ historyVersion: cameras[0]?.historyVersion ?? 0,
32480
+ environmentSignature: environment?.environmentSignature ?? "",
32481
+ fogSignature: environment?.fogSignature ?? "",
32482
+ deviceGeneration: internals.deviceScope?.generation ?? 0
32483
+ });
32484
+ const demandedMotionBlur = motionBlurPlan.demanded;
32485
+ motionBlurFeatureInput = motionBlurPlan.featureInput;
32486
+ if (demandedMotionBlur && internals.device.caps.compute === true && internals.device.caps.storageBuffer === true && internals.device.caps.storageTexture === true && internals.device.caps.rgba16floatRenderable === true) {
32487
+ ensureMotionBlurFeature();
32488
+ }
30166
32489
  preparedFeatureSkylightResources = preparedPipelineState === null ? void 0 : resolveMaterialSkylight(internals, preparedPipelineState, skylight, skylightCount).skylightResources;
30167
32490
  let featureGraphCandidate;
30168
32491
  const preparedResourceBatches = runProfiledRenderPhase(profileSession, "features", () => {
@@ -30171,6 +32494,7 @@ function createRenderSystem(internals) {
30171
32494
  worlds: compositionWorlds,
30172
32495
  owner: resourceOwner,
30173
32496
  frameNumber: frameState.frameNumber,
32497
+ frameSize: { width: internals.canvas.width, height: internals.canvas.height },
30174
32498
  visibilitySnapshots: frame.featureVisibilitySnapshots,
30175
32499
  hiddenEntityReports: frame.hiddenEntityReports,
30176
32500
  targets: featureTargets,
@@ -30199,11 +32523,12 @@ function createRenderSystem(internals) {
30199
32523
  gpuWork: featureGpuWork
30200
32524
  });
30201
32525
  lastVisibilityStats.explicitlyHidden = featureFrame.hiddenEntityReports.length;
30202
- for (const featureError of featureFrame.errors) {
30203
- if (!isPendingRenderFeaturePreparation(featureError)) {
30204
- internals.errorRegistry.fire(featureError);
30205
- }
30206
- }
32526
+ reportPendingRenderFeatureErrors(
32527
+ featureFrame.errors,
32528
+ frameState.frameNumber,
32529
+ pendingFeatureErrorLastReportedFrame,
32530
+ internals.errorRegistry
32531
+ );
30207
32532
  const postProcessParamsCandidate = preparePostProcessParamsResources(
30208
32533
  featureFrame.fullscreenEffects
30209
32534
  );
@@ -30287,7 +32612,8 @@ function createRenderSystem(internals) {
30287
32612
  }
30288
32613
  return runProfiledRenderPhase(profileSession, phase, action);
30289
32614
  };
30290
- if (cameras[0]?.bloom === "on") {
32615
+ const bloomAdmitted = standardBloomAdmitted(cameras[0]);
32616
+ if (bloomAdmitted) {
30291
32617
  internals.getPipelineState()?.perPassResources.ensureBloomResources?.();
30292
32618
  }
30293
32619
  const liveOutput = cameras[0]?.output;
@@ -30330,6 +32656,10 @@ function createRenderSystem(internals) {
30330
32656
  activeEntityKeys: visibilityProjection.activeEntityKeys,
30331
32657
  activeEntityRevision: visibilityProjection.activeEntityRevision,
30332
32658
  telemetryCandidateCount: lodCandidateCountForFrame,
32659
+ telemetrySubmit: {
32660
+ frameId: lodSubmitFrameId,
32661
+ deviceGeneration: internals.deviceScope.generation
32662
+ },
30333
32663
  ...occlusionProjection === void 0 ? {} : { occlusion: occlusionProjection }
30334
32664
  },
30335
32665
  compositionLeases,
@@ -30355,7 +32685,10 @@ function createRenderSystem(internals) {
30355
32685
  worldKeys,
30356
32686
  void 0,
30357
32687
  ssrDependencies,
30358
- dynamicGeometryFrames.onRenderableDraw(compositionWorlds)
32688
+ dynamicGeometryFrames.onRenderableDraw(compositionWorlds),
32689
+ environment?.fog,
32690
+ opts.sampleTimeSeconds,
32691
+ opts.temporalReset === true
30359
32692
  )
30360
32693
  );
30361
32694
  dynamicGeometryFrames.commit(submitted);
@@ -30433,7 +32766,7 @@ function createRenderSystem(internals) {
30433
32766
  frameState.depthOfFieldLastSubmitFailed = true;
30434
32767
  }
30435
32768
  internals.getPipelineState()?.perPassResources.commitBloomResources?.();
30436
- const nextBloom = cameras[0]?.bloom ?? "off";
32769
+ const nextBloom = bloomAdmitted ? "on" : "off";
30437
32770
  if (frameState.lastSuccessfulBloom === "on" && nextBloom !== "on") {
30438
32771
  internals.getPipelineState()?.perPassResources.retireBloomResources?.(
30439
32772
  internals.device.queue.onSubmittedWorkDone()
@@ -30491,6 +32824,7 @@ function createRenderSystem(internals) {
30491
32824
  })
30492
32825
  );
30493
32826
  } finally {
32827
+ if (!submitted) persistentRenderScene.discardTemporalFrame();
30494
32828
  internals.recoveryColdWorkGuard?.endFrame();
30495
32829
  if (ownsProfileFrame) {
30496
32830
  try {
@@ -30507,13 +32841,26 @@ function createRenderSystem(internals) {
30507
32841
  const currentFrameId = frameState.frameNumber - 1;
30508
32842
  return observeCurrentFrame(options, frameState.currentFrameObservationSource, currentFrameId);
30509
32843
  },
30510
- async observeLodOcclusion() {
32844
+ async observeLodOcclusion(receipt) {
32845
+ const capturedInspection = lastLodOcclusionInspection;
32846
+ const capturedWorldAttachments = lastLodWorldAttachments;
32847
+ if (capturedInspection === void 0) return;
32848
+ if (receipt !== void 0 && (capturedInspection.submit.frameId !== receipt.frameId || capturedInspection.submit.deviceGeneration !== receipt.deviceGeneration))
32849
+ return;
30511
32850
  await ensureOcclusionRuntime().waitForCompletions();
30512
- applyLodSelectionTelemetry(await gpuDrivenProduction.readLodSelection());
30513
- if (lastLodOcclusionInspection !== void 0) {
32851
+ if (lastLodOcclusionInspection !== capturedInspection) return;
32852
+ const selection = await gpuDrivenProduction.readLodSelection();
32853
+ if (lastLodOcclusionInspection !== capturedInspection || selection === void 0) return;
32854
+ if (selection.submit !== void 0 && (selection.submit.frameId !== capturedInspection.submit.frameId || selection.submit.deviceGeneration !== capturedInspection.submit.deviceGeneration))
32855
+ return;
32856
+ if (receipt !== void 0 && (selection.submit === void 0 || selection.submit.frameId !== receipt.frameId || selection.submit.deviceGeneration !== receipt.deviceGeneration))
32857
+ return;
32858
+ applyLodSelectionTelemetry(selection, capturedInspection, capturedWorldAttachments);
32859
+ const published = lastLodOcclusionInspection;
32860
+ if (published !== void 0 && published.submit.frameId === capturedInspection.submit.frameId && published.submit.deviceGeneration === capturedInspection.submit.deviceGeneration) {
30514
32861
  const queryLatency = ensureOcclusionRuntime().inspectQueryLatency();
30515
32862
  lastLodOcclusionInspection = inspectLodOcclusion({
30516
- ...lastLodOcclusionInspection,
32863
+ ...published,
30517
32864
  queryLatencyUs: {
30518
32865
  median: queryLatency.median,
30519
32866
  p95: queryLatency.p95,
@@ -31727,6 +34074,16 @@ function tilemapChunkExtractSystem(world, lookup = () => void 0) {
31727
34074
  }
31728
34075
  }
31729
34076
  }
34077
+
34078
+ // src/assembly/analytic-fog-registration.ts
34079
+ function registerAnalyticFogPostProcess(renderSystem, source, candidate) {
34080
+ if (candidate) return;
34081
+ renderSystem.registerBuiltinPostProcess("forgeax.analytic-fog", {
34082
+ source,
34083
+ params: { byteSize: 112, defaultValue: new Uint8Array(112) },
34084
+ reads: ["scene-color", { key: "scene-depth", sampleType: "depth" }]
34085
+ });
34086
+ }
31730
34087
  function fixedStepOf(world) {
31731
34088
  try {
31732
34089
  return world.getResource(FixedTime).tick;
@@ -32433,6 +34790,25 @@ function createRendererDynamicGeometryController(options) {
32433
34790
  host: createRendererDynamicGeometryHost({ ...options, lifecycle })
32434
34791
  };
32435
34792
  }
34793
+
34794
+ // src/assembly/fxaa-registration.ts
34795
+ function registerFxaaPostProcess(renderSystem, source, registered) {
34796
+ if (registered) return true;
34797
+ renderSystem.registerBuiltinPostProcess(FXAA_POST_PROCESS_ID, {
34798
+ source,
34799
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
34800
+ reads: ["ldrColor"]
34801
+ });
34802
+ return true;
34803
+ }
34804
+
34805
+ // src/assembly/lod-observation.ts
34806
+ function observeLodOcclusionForReceipt(renderSystem, receipt) {
34807
+ return renderSystem.observeLodOcclusion({
34808
+ frameId: receipt.frameId,
34809
+ deviceGeneration: receipt.deviceGeneration
34810
+ });
34811
+ }
32436
34812
  function makeShaderDeviceAdapter(rhiDevice, _errorRegistry, asyncCreateShaderModule, immediateCreateShaderModule, mode = "validated") {
32437
34813
  const moduleCache = /* @__PURE__ */ new Map();
32438
34814
  const errorCache = /* @__PURE__ */ new Map();
@@ -33076,6 +35452,18 @@ function resolveMaterialPipelineBindGroups(layoutKind, state, clustered = false)
33076
35452
  function isEngineOwnedMaterialShader(materialShaderId) {
33077
35453
  return materialShaderId === "forgeax::default-unlit" || materialShaderId === "forgeax::default-shadow-caster" || materialShaderId === "forgeax::sprite" || materialShaderId === "forgeax::sprite-lit" || materialShaderId === "forgeax::default-sprite" || materialShaderId === "forgeax::msdf-text" || materialShaderId === "forgeax::default-standard-pbr" || materialShaderId === "forgeax::pbr-skin" || materialShaderId === "forgeax::default-standard-pbr-skin";
33078
35454
  }
35455
+
35456
+ // src/assembly/motion-blur-registration.ts
35457
+ function registerMotionBlurPostProcess(renderSystem, source) {
35458
+ renderSystem.registerBuiltinPostProcess("forgeax.motion-blur", {
35459
+ source,
35460
+ params: {
35461
+ byteSize: MOTION_BLUR_PARAMS_BYTE_SIZE,
35462
+ defaultValue: new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE)
35463
+ },
35464
+ reads: ["scene-color", "scene-temporal"]
35465
+ });
35466
+ }
33079
35467
  var ROOT_ORDER = /* @__PURE__ */ new Map([
33080
35468
  ["listener", 0],
33081
35469
  ["surface", 1],
@@ -36384,6 +38772,27 @@ function createMeshSsboGrowController(init) {
36384
38772
  };
36385
38773
  return { state, initialBuild, growMeshSsbo };
36386
38774
  }
38775
+
38776
+ // src/assembly/temporal-post-process-prewarm.ts
38777
+ function createTemporalShaderPrewarmer(input) {
38778
+ return async (entry, id) => {
38779
+ if (entry === void 0) return;
38780
+ const prewarm = await runShimStep(
38781
+ () => input.asyncCreateShaderModule ? input.asyncCreateShaderModule(input.device, {
38782
+ code: entry.wgsl,
38783
+ label: postProcessShaderModuleLabel(id, entry.wgsl)
38784
+ }) : invokeDeviceCreateShaderModule(input.device, {
38785
+ code: entry.wgsl,
38786
+ label: postProcessShaderModuleLabel(id, entry.wgsl)
38787
+ }),
38788
+ "shader-compile-failed",
38789
+ `${id} shader module compiled (unified post-process prewarm)`,
38790
+ `inspect manifest ${id} entry composed wgsl; check device.features`
38791
+ );
38792
+ if (!prewarm.ok) throw prewarm.error;
38793
+ input.seedShaderModule(postProcessShaderModuleLabel(id, entry.wgsl), prewarm.value);
38794
+ };
38795
+ }
36387
38796
  function createShader(input, code, label) {
36388
38797
  return input.asyncCreateShaderModule ? input.asyncCreateShaderModule(input.device, { code, label }) : Promise.resolve(invokeDeviceCreateShaderModule(input.device, { code, label }));
36389
38798
  }
@@ -36516,19 +38925,6 @@ function buildGpuDrivenPbrReadyLayouts(device, storageBufferCapable, pbrLayouts)
36516
38925
  };
36517
38926
  }
36518
38927
 
36519
- // src/assembly/webgpu-ready-contract.ts
36520
- var VIEW_UBO_BYTES = VIEW_UNIFORM_BUFFER_SIZE;
36521
- var MIPMAP_PREWARM_FORMATS = [
36522
- "rgba8unorm-srgb",
36523
- "rgba8unorm",
36524
- "rgba16float"
36525
- ];
36526
- var BRIGHT_PARAMS_BYTES = 16;
36527
- var BLUR_PARAMS_BYTES = 16;
36528
- var COMPOSITE_PARAMS_BYTES = 16;
36529
- var HDR_COLOR_ATTACHMENT_FORMAT = "rgba16float";
36530
- var DEPTH_TEXTURE_FORMAT = "depth24plus-stencil8";
36531
-
36532
38928
  // src/assembly/webgpu-ready-mipmap.ts
36533
38929
  function adaptMipmapShaderModuleFactory(factory) {
36534
38930
  if (factory === void 0) return void 0;
@@ -36541,7 +38937,7 @@ async function prewarmMipmapPipeline(rhiDevice, gpuStore, hasMaterialManifest) {
36541
38937
  }
36542
38938
 
36543
38939
  // src/assembly/webgpu-ready.ts
36544
- async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, asyncCreateShaderModule, errorRegistry, requiredMaterialShaders, standardPbrShaderUvSetCount, requiredFullscreenPostProcesses, setGrowMeshSsboHook, seedShaderModule, seedMaterialShaderPipelineCache, registerBuiltinTonemap, registerBuiltinFxaa, registerBuiltinTemporalPostProcesses, registerBuiltinDepthOfField, setVolumetricFogShaderSources, setSsrShaderSources, setAtmosphereShaderSources) {
38940
+ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, asyncCreateShaderModule, errorRegistry, requiredMaterialShaders, standardPbrShaderUvSetCount, requiredFullscreenPostProcesses, setGrowMeshSsboHook, seedShaderModule, seedMaterialShaderPipelineCache, registerBuiltinTonemap, registerBuiltinFxaa, registerBuiltinAnalyticFog, registerBuiltinTemporalPostProcesses, registerBuiltinDepthOfField, setVolumetricFogShaderSources, setSsrShaderSources, setAtmosphereShaderSources) {
36545
38941
  const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
36546
38942
  const capValue = rhiDevice.limits.maxStorageBuffersPerShaderStage;
36547
38943
  const webgl2Downlevel = rhiDevice.caps.backendKind === "wgpu-webgl2";
@@ -36576,8 +38972,8 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
36576
38972
  let spriteLitModule = null;
36577
38973
  let fxaaModule = null;
36578
38974
  let skyboxModule = null;
36579
- let bloomBrightModule = null;
36580
- let bloomBlurModule = null;
38975
+ let bloomDownsampleModule = null;
38976
+ let bloomUpsampleModule = null;
36581
38977
  let bloomCompositeModule = null;
36582
38978
  let ssaoModule = null;
36583
38979
  const findEngineManifestEntry = (identifier) => {
@@ -36608,8 +39004,8 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
36608
39004
  let probeBackgroundEntry;
36609
39005
  let fxaaEntry;
36610
39006
  let skyboxEntry;
36611
- let bloomBrightEntry;
36612
- let bloomBlurEntry;
39007
+ let bloomDownsampleEntry;
39008
+ let bloomUpsampleEntry;
36613
39009
  let bloomCompositeEntry;
36614
39010
  let ssaoEntry;
36615
39011
  let shadowCasterEntry = findEngineManifestEntry("forgeax::default-shadow-caster");
@@ -36666,12 +39062,12 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
36666
39062
  skyboxEntry ??= entry;
36667
39063
  continue;
36668
39064
  }
36669
- if (entry.wgsl.includes("BloomBrightParams")) {
36670
- bloomBrightEntry ??= entry;
39065
+ if (entry.wgsl.includes("BloomDownsampleParams")) {
39066
+ bloomDownsampleEntry ??= entry;
36671
39067
  continue;
36672
39068
  }
36673
- if (entry.wgsl.includes("BloomBlurParams")) {
36674
- bloomBlurEntry ??= entry;
39069
+ if (entry.wgsl.includes("BloomUpsampleParams")) {
39070
+ bloomUpsampleEntry ??= entry;
36675
39071
  continue;
36676
39072
  }
36677
39073
  if (entry.wgsl.includes("BloomCompositeParams")) {
@@ -37127,23 +39523,14 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
37127
39523
  tonemapPrewarm.value
37128
39524
  );
37129
39525
  registerBuiltinTonemap(tonemapEntry.wgsl);
37130
- const prewarmTemporal = async (entry, id) => {
37131
- if (entry === void 0) return;
37132
- const prewarm = await runShimStep(
37133
- () => asyncCreateShaderModule ? asyncCreateShaderModule(rhiDevice, {
37134
- code: entry.wgsl,
37135
- label: postProcessShaderModuleLabel(id, entry.wgsl)
37136
- }) : invokeDeviceCreateShaderModule(rhiDevice, {
37137
- code: entry.wgsl,
37138
- label: postProcessShaderModuleLabel(id, entry.wgsl)
37139
- }),
37140
- "shader-compile-failed",
37141
- `${id} shader module compiled (unified post-process prewarm)`,
37142
- `inspect manifest ${id} entry composed wgsl; check device.features`
37143
- );
37144
- if (!prewarm.ok) throw prewarm.error;
37145
- seedShaderModule(postProcessShaderModuleLabel(id, entry.wgsl), prewarm.value);
37146
- };
39526
+ const prewarmTemporal = createTemporalShaderPrewarmer({
39527
+ device: rhiDevice,
39528
+ asyncCreateShaderModule,
39529
+ seedShaderModule
39530
+ });
39531
+ const analyticFogEntry = findEngineManifestEntry("forgeax::analytic-fog");
39532
+ await prewarmTemporal(analyticFogEntry, "forgeax.analytic-fog");
39533
+ if (analyticFogEntry !== void 0) registerBuiltinAnalyticFog(analyticFogEntry.wgsl);
37147
39534
  await prewarmTemporal(motionBlurEntry, "forgeax.motion-blur");
37148
39535
  await prewarmTemporal(taaResolveEntry, "forgeax.taa-resolve");
37149
39536
  await prewarmDepthOfField(manifestEntries, prewarmTemporal, registerBuiltinDepthOfField);
@@ -37264,34 +39651,37 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
37264
39651
  if (!skyboxShaderResult.ok) throw skyboxShaderResult.error;
37265
39652
  skyboxModule = skyboxShaderResult.value;
37266
39653
  }
37267
- if (bloomBrightEntry !== void 0) {
39654
+ if (bloomDownsampleEntry !== void 0) {
37268
39655
  const result = await runShimStep(
37269
39656
  () => asyncCreateShaderModule ? asyncCreateShaderModule(rhiDevice, {
37270
- code: bloomBrightEntry.wgsl,
37271
- label: "bloom-bright"
39657
+ code: bloomDownsampleEntry.wgsl,
39658
+ label: "bloom-downsample"
37272
39659
  }) : invokeDeviceCreateShaderModule(rhiDevice, {
37273
- code: bloomBrightEntry.wgsl,
37274
- label: "bloom-bright"
39660
+ code: bloomDownsampleEntry.wgsl,
39661
+ label: "bloom-downsample"
37275
39662
  }),
37276
39663
  "shader-compile-failed",
37277
- "bloom-bright shader module compiled",
37278
- "inspect manifest bloom-bright entry composed wgsl; check device.features"
39664
+ "bloom-downsample shader module compiled",
39665
+ "inspect manifest bloom-downsample entry composed wgsl; check device.features"
37279
39666
  );
37280
39667
  if (!result.ok) throw result.error;
37281
- bloomBrightModule = result.value;
39668
+ bloomDownsampleModule = result.value;
37282
39669
  }
37283
- if (bloomBlurEntry !== void 0) {
39670
+ if (bloomUpsampleEntry !== void 0) {
37284
39671
  const result = await runShimStep(
37285
- () => asyncCreateShaderModule ? asyncCreateShaderModule(rhiDevice, { code: bloomBlurEntry.wgsl, label: "bloom-blur" }) : invokeDeviceCreateShaderModule(rhiDevice, {
37286
- code: bloomBlurEntry.wgsl,
37287
- label: "bloom-blur"
39672
+ () => asyncCreateShaderModule ? asyncCreateShaderModule(rhiDevice, {
39673
+ code: bloomUpsampleEntry.wgsl,
39674
+ label: "bloom-upsample"
39675
+ }) : invokeDeviceCreateShaderModule(rhiDevice, {
39676
+ code: bloomUpsampleEntry.wgsl,
39677
+ label: "bloom-upsample"
37288
39678
  }),
37289
39679
  "shader-compile-failed",
37290
- "bloom-blur shader module compiled",
37291
- "inspect manifest bloom-blur entry composed wgsl; check device.features"
39680
+ "bloom-upsample shader module compiled",
39681
+ "inspect manifest bloom-upsample entry composed wgsl; check device.features"
37292
39682
  );
37293
39683
  if (!result.ok) throw result.error;
37294
- bloomBlurModule = result.value;
39684
+ bloomUpsampleModule = result.value;
37295
39685
  }
37296
39686
  if (bloomCompositeEntry !== void 0) {
37297
39687
  const result = await runShimStep(
@@ -37979,10 +40369,10 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
37979
40369
  } else if (fragModule === skyboxModule) {
37980
40370
  const msaa = d.multisample;
37981
40371
  label = msaa !== void 0 ? "skybox-pipeline-msaa" : "skybox-pipeline";
37982
- } else if (fragModule === bloomBrightModule) {
37983
- label = "bloom-bright-pipeline";
37984
- } else if (fragModule === bloomBlurModule) {
37985
- label = d.label ?? "bloom-blur-h-pipeline";
40372
+ } else if (fragModule === bloomDownsampleModule) {
40373
+ label = "bloom-downsample-pipeline";
40374
+ } else if (fragModule === bloomUpsampleModule) {
40375
+ label = "bloom-upsample-pipeline";
37986
40376
  } else if (fragModule === bloomCompositeModule) {
37987
40377
  label = "bloom-composite-pipeline";
37988
40378
  } else if (fragModule === ssaoModule) {
@@ -38065,17 +40455,15 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38065
40455
  let skyboxSamplerHandle = null;
38066
40456
  let skyboxRotationBufferHandle = null;
38067
40457
  let skyboxPipelineMsaaHandle = null;
38068
- let bloomBrightPipelineHandle = null;
38069
- let bloomBlurHPipelineHandle = null;
38070
- let bloomBlurVPipelineHandle = null;
40458
+ let bloomDownsamplePipelineHandle = null;
40459
+ let bloomUpsamplePipelineHandle = null;
38071
40460
  let bloomCompositePipelineHandle = null;
38072
- let bloomBrightBglHandle = null;
38073
- let bloomBlurBglHandle = null;
40461
+ let bloomDownsampleBglHandle = null;
40462
+ let bloomUpsampleBglHandle = null;
38074
40463
  let bloomCompositeBglHandle = null;
38075
40464
  let bloomSamplerHandle = null;
38076
- let bloomBrightParamsBufferHandle = null;
38077
- let bloomBlurHParamsBufferHandle = null;
38078
- let bloomBlurVParamsBufferHandle = null;
40465
+ let bloomDownsampleParamsBufferHandle = null;
40466
+ let bloomUpsampleParamsBufferHandle = null;
38079
40467
  let bloomCompositeParamsBufferHandle = null;
38080
40468
  let bloomResourcesReady = false;
38081
40469
  let ensureBloomResources;
@@ -38106,20 +40494,7 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38106
40494
  drainBloomResources?.();
38107
40495
  };
38108
40496
  const inspectBloomResourcesStable = (graph) => {
38109
- return inspectBloomResources?.(graph) ?? {
38110
- graphStatus: "empty",
38111
- enabled: false,
38112
- targetCount: 0,
38113
- targetBytes: 0,
38114
- resourceCount: 0,
38115
- passCount: 0,
38116
- encodeCount: 0,
38117
- bindGroupCount: 0,
38118
- uploadCount: 0,
38119
- residentChildBytes: 0,
38120
- generation: 0,
38121
- state: "off"
38122
- };
40497
+ return inspectBloomResources?.(graph) ?? emptyBloomInspection();
38123
40498
  };
38124
40499
  let ssaoCalcPipelineHandle = null;
38125
40500
  let ssaoBlurPipelineHandle = null;
@@ -38356,7 +40731,7 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38356
40731
  return value;
38357
40732
  };
38358
40733
  try {
38359
- if (bloomBrightModule !== null || bloomBlurModule !== null || bloomCompositeModule !== null) {
40734
+ if (bloomDownsampleModule !== null || bloomUpsampleModule !== null || bloomCompositeModule !== null) {
38360
40735
  const bloomSamplerResult = runShimSyncStep(
38361
40736
  () => rhiDevice.createSampler({
38362
40737
  label: "bloom-sampler",
@@ -38373,10 +40748,10 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38373
40748
  if (!bloomSamplerResult.ok) throw bloomSamplerResult.error;
38374
40749
  bloomSamplerHandle = adopt("binding", bloomSamplerResult.value, () => void 0);
38375
40750
  }
38376
- if (bloomBrightModule !== null && bloomSamplerHandle !== null) {
38377
- const brightBglResult = runShimSyncStep(
40751
+ if (bloomDownsampleModule !== null && bloomSamplerHandle !== null) {
40752
+ const downsampleBglResult = runShimSyncStep(
38378
40753
  () => rhiDevice.createBindGroupLayout({
38379
- label: "bloom-bright-bgl",
40754
+ label: "bloom-downsample-bgl",
38380
40755
  entries: [
38381
40756
  {
38382
40757
  binding: 0,
@@ -38396,26 +40771,26 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38396
40771
  ]
38397
40772
  }),
38398
40773
  "webgpu-runtime-error",
38399
- "createBindGroupLayout(bloom-bright) succeeded",
40774
+ "createBindGroupLayout(bloom-downsample) succeeded",
38400
40775
  "check device.limits.maxBindGroupsPerPipelineLayout"
38401
40776
  );
38402
- if (!brightBglResult.ok) throw brightBglResult.error;
38403
- bloomBrightBglHandle = adopt("binding", brightBglResult.value, () => void 0);
38404
- const brightPlResult = runShimSyncStep(
40777
+ if (!downsampleBglResult.ok) throw downsampleBglResult.error;
40778
+ bloomDownsampleBglHandle = adopt("binding", downsampleBglResult.value, () => void 0);
40779
+ const downsamplePlResult = runShimSyncStep(
38405
40780
  () => rhiDevice.createPipelineLayout({
38406
- label: "bloom-bright-pl",
38407
- bindGroupLayouts: [brightBglResult.value]
40781
+ label: "bloom-downsample-pl",
40782
+ bindGroupLayouts: [downsampleBglResult.value]
38408
40783
  }),
38409
40784
  "webgpu-runtime-error",
38410
- "createPipelineLayout(bloom-bright) succeeded",
38411
- "verify bloom-bright BindGroupLayout matches shader @group(0) bindings"
40785
+ "createPipelineLayout(bloom-downsample) succeeded",
40786
+ "verify bloom-downsample BindGroupLayout matches shader @group(0) bindings"
38412
40787
  );
38413
- if (!brightPlResult.ok) throw brightPlResult.error;
38414
- adopt("binding", brightPlResult.value, () => void 0);
40788
+ if (!downsamplePlResult.ok) throw downsamplePlResult.error;
40789
+ adopt("binding", downsamplePlResult.value, () => void 0);
38415
40790
  {
38416
- const brightSpec = {
40791
+ const downsampleSpec = {
38417
40792
  shader: {
38418
- id: "forgeax::post::bloom-bright",
40793
+ id: "forgeax::post::bloom-downsample",
38419
40794
  passKind: "post-process",
38420
40795
  variantSet: void 0
38421
40796
  },
@@ -38432,20 +40807,21 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38432
40807
  renderState: { cullMode: "none" }
38433
40808
  };
38434
40809
  const modules = {
38435
- vertex: bloomBrightModule,
38436
- fragment: bloomBrightModule,
38437
- layout: brightPlResult.value
40810
+ vertex: bloomDownsampleModule,
40811
+ fragment: bloomDownsampleModule,
40812
+ layout: downsamplePlResult.value,
40813
+ label: "bloom-downsample-pipeline"
38438
40814
  };
38439
40815
  try {
38440
- bloomBrightPipelineHandle = getOrBuildPipeline(
38441
- brightSpec,
40816
+ bloomDownsamplePipelineHandle = getOrBuildPipeline(
40817
+ downsampleSpec,
38442
40818
  pipelineDeviceProvider,
38443
40819
  pipelineCache,
38444
40820
  modules
38445
40821
  );
38446
- bloomBrightPipelineHandle = adopt(
40822
+ bloomDownsamplePipelineHandle = adopt(
38447
40823
  "pipeline",
38448
- bloomBrightPipelineHandle,
40824
+ bloomDownsamplePipelineHandle,
38449
40825
  () => void 0
38450
40826
  );
38451
40827
  } catch (err54) {
@@ -38453,30 +40829,34 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38453
40829
  throw new PipelineSpecError({
38454
40830
  code: "pipeline-build-failed",
38455
40831
  detail: { cause: err54 },
38456
- hint: "createRenderPipeline (bloom-bright fullscreen) failed; inspect gpuMessage"
40832
+ hint: "createRenderPipeline (bloom-downsample fullscreen) failed; inspect gpuMessage"
38457
40833
  });
38458
40834
  }
38459
40835
  }
38460
- const brightParamsResult = runShimSyncStep(
40836
+ const downsampleParamsResult = runShimSyncStep(
38461
40837
  () => rhiDevice.createBuffer({
38462
- label: "bloom-bright-params-ubo",
38463
- size: BRIGHT_PARAMS_BYTES,
40838
+ label: "bloom-downsample-params-ubo",
40839
+ size: BLOOM_DOWNSAMPLE_PARAMS_BYTES,
38464
40840
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
38465
40841
  mappedAtCreation: false
38466
40842
  }),
38467
40843
  "webgpu-runtime-error",
38468
- "createBuffer (bloom-bright params UBO) succeeded",
40844
+ "createBuffer (bloom-downsample params UBO) succeeded",
38469
40845
  "check device.limits.maxUniformBufferBindingSize"
38470
40846
  );
38471
- if (!brightParamsResult.ok) throw brightParamsResult.error;
38472
- bloomBrightParamsBufferHandle = adopt("buffer", brightParamsResult.value, (value) => {
38473
- rhiDevice.destroyBuffer(value);
38474
- });
40847
+ if (!downsampleParamsResult.ok) throw downsampleParamsResult.error;
40848
+ bloomDownsampleParamsBufferHandle = adopt(
40849
+ "buffer",
40850
+ downsampleParamsResult.value,
40851
+ (value) => {
40852
+ rhiDevice.destroyBuffer(value);
40853
+ }
40854
+ );
38475
40855
  }
38476
- if (bloomBlurModule !== null && bloomSamplerHandle !== null) {
38477
- const blurBglResult = runShimSyncStep(
40856
+ if (bloomUpsampleModule !== null && bloomSamplerHandle !== null) {
40857
+ const upsampleBglResult = runShimSyncStep(
38478
40858
  () => rhiDevice.createBindGroupLayout({
38479
- label: "bloom-blur-bgl",
40859
+ label: "bloom-upsample-bgl",
38480
40860
  entries: [
38481
40861
  {
38482
40862
  binding: 0,
@@ -38486,36 +40866,41 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38486
40866
  {
38487
40867
  binding: 1,
38488
40868
  visibility: GPU_SHADER_STAGE_FRAGMENT,
38489
- sampler: { type: "filtering" }
40869
+ texture: { sampleType: "float", viewDimension: "2d" }
38490
40870
  },
38491
40871
  {
38492
40872
  binding: 2,
38493
40873
  visibility: GPU_SHADER_STAGE_FRAGMENT,
40874
+ sampler: { type: "filtering" }
40875
+ },
40876
+ {
40877
+ binding: 3,
40878
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
38494
40879
  buffer: { type: "uniform" }
38495
40880
  }
38496
40881
  ]
38497
40882
  }),
38498
40883
  "webgpu-runtime-error",
38499
- "createBindGroupLayout(bloom-blur) succeeded",
40884
+ "createBindGroupLayout(bloom-upsample) succeeded",
38500
40885
  "check device.limits.maxBindGroupsPerPipelineLayout"
38501
40886
  );
38502
- if (!blurBglResult.ok) throw blurBglResult.error;
38503
- bloomBlurBglHandle = adopt("binding", blurBglResult.value, () => void 0);
38504
- const blurPlResult = runShimSyncStep(
40887
+ if (!upsampleBglResult.ok) throw upsampleBglResult.error;
40888
+ bloomUpsampleBglHandle = adopt("binding", upsampleBglResult.value, () => void 0);
40889
+ const upsamplePlResult = runShimSyncStep(
38505
40890
  () => rhiDevice.createPipelineLayout({
38506
- label: "bloom-blur-pl",
38507
- bindGroupLayouts: [blurBglResult.value]
40891
+ label: "bloom-upsample-pl",
40892
+ bindGroupLayouts: [upsampleBglResult.value]
38508
40893
  }),
38509
40894
  "webgpu-runtime-error",
38510
- "createPipelineLayout(bloom-blur) succeeded",
38511
- "verify bloom-blur BindGroupLayout matches shader @group(0) bindings"
40895
+ "createPipelineLayout(bloom-upsample) succeeded",
40896
+ "verify bloom-upsample BindGroupLayout matches shader @group(0) bindings"
38512
40897
  );
38513
- if (!blurPlResult.ok) throw blurPlResult.error;
38514
- adopt("binding", blurPlResult.value, () => void 0);
40898
+ if (!upsamplePlResult.ok) throw upsamplePlResult.error;
40899
+ adopt("binding", upsamplePlResult.value, () => void 0);
38515
40900
  {
38516
- const blurSpec = {
40901
+ const upsampleSpec = {
38517
40902
  shader: {
38518
- id: "forgeax::post::bloom-blur",
40903
+ id: "forgeax::post::bloom-upsample",
38519
40904
  passKind: "post-process",
38520
40905
  variantSet: void 0
38521
40906
  },
@@ -38532,21 +40917,21 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38532
40917
  renderState: { cullMode: "none" }
38533
40918
  };
38534
40919
  const modules = {
38535
- vertex: bloomBlurModule,
38536
- fragment: bloomBlurModule,
38537
- layout: blurPlResult.value,
38538
- label: "bloom-blur-h-pipeline"
40920
+ vertex: bloomUpsampleModule,
40921
+ fragment: bloomUpsampleModule,
40922
+ layout: upsamplePlResult.value,
40923
+ label: "bloom-upsample-pipeline"
38539
40924
  };
38540
40925
  try {
38541
- bloomBlurHPipelineHandle = getOrBuildPipeline(
38542
- blurSpec,
40926
+ bloomUpsamplePipelineHandle = getOrBuildPipeline(
40927
+ upsampleSpec,
38543
40928
  pipelineDeviceProvider,
38544
40929
  pipelineCache,
38545
40930
  modules
38546
40931
  );
38547
- bloomBlurHPipelineHandle = adopt(
40932
+ bloomUpsamplePipelineHandle = adopt(
38548
40933
  "pipeline",
38549
- bloomBlurHPipelineHandle,
40934
+ bloomUpsamplePipelineHandle,
38550
40935
  () => void 0
38551
40936
  );
38552
40937
  } catch (err54) {
@@ -38554,39 +40939,23 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38554
40939
  throw new PipelineSpecError({
38555
40940
  code: "pipeline-build-failed",
38556
40941
  detail: { cause: err54 },
38557
- hint: "createRenderPipeline (bloom-blur-h fullscreen) failed; inspect gpuMessage"
40942
+ hint: "createRenderPipeline (bloom-upsample fullscreen) failed; inspect gpuMessage"
38558
40943
  });
38559
40944
  }
38560
- bloomBlurVPipelineHandle = bloomBlurHPipelineHandle;
38561
40945
  }
38562
- const blurHParamsResult = runShimSyncStep(
38563
- () => rhiDevice.createBuffer({
38564
- label: "bloom-blur-h-params-ubo",
38565
- size: BLUR_PARAMS_BYTES,
38566
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
38567
- mappedAtCreation: false
38568
- }),
38569
- "webgpu-runtime-error",
38570
- "createBuffer (bloom-blur-h params UBO) succeeded",
38571
- "check device.limits.maxUniformBufferBindingSize"
38572
- );
38573
- if (!blurHParamsResult.ok) throw blurHParamsResult.error;
38574
- bloomBlurHParamsBufferHandle = adopt("buffer", blurHParamsResult.value, (value) => {
38575
- rhiDevice.destroyBuffer(value);
38576
- });
38577
- const blurVParamsResult = runShimSyncStep(
40946
+ const upsampleParamsResult = runShimSyncStep(
38578
40947
  () => rhiDevice.createBuffer({
38579
- label: "bloom-blur-v-params-ubo",
38580
- size: BLUR_PARAMS_BYTES,
40948
+ label: "bloom-upsample-params-ubo",
40949
+ size: BLOOM_UPSAMPLE_PARAMS_BYTES,
38581
40950
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
38582
40951
  mappedAtCreation: false
38583
40952
  }),
38584
40953
  "webgpu-runtime-error",
38585
- "createBuffer (bloom-blur-v params UBO) succeeded",
40954
+ "createBuffer (bloom-upsample params UBO) succeeded",
38586
40955
  "check device.limits.maxUniformBufferBindingSize"
38587
40956
  );
38588
- if (!blurVParamsResult.ok) throw blurVParamsResult.error;
38589
- bloomBlurVParamsBufferHandle = adopt("buffer", blurVParamsResult.value, (value) => {
40957
+ if (!upsampleParamsResult.ok) throw upsampleParamsResult.error;
40958
+ bloomUpsampleParamsBufferHandle = adopt("buffer", upsampleParamsResult.value, (value) => {
38590
40959
  rhiDevice.destroyBuffer(value);
38591
40960
  });
38592
40961
  }
@@ -38682,7 +41051,7 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38682
41051
  const compositeParamsResult = runShimSyncStep(
38683
41052
  () => rhiDevice.createBuffer({
38684
41053
  label: "bloom-composite-params-ubo",
38685
- size: COMPOSITE_PARAMS_BYTES,
41054
+ size: BLOOM_COMPOSITE_PARAMS_BYTES,
38686
41055
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
38687
41056
  mappedAtCreation: false
38688
41057
  }),
@@ -38702,17 +41071,15 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38702
41071
  bloomCandidateBundle = {
38703
41072
  scope: candidateScope,
38704
41073
  generation: bloomGeneration + 1,
38705
- bloomBrightPipeline: bloomBrightPipelineHandle,
38706
- bloomBlurHPipeline: bloomBlurHPipelineHandle,
38707
- bloomBlurVPipeline: bloomBlurVPipelineHandle,
41074
+ bloomDownsamplePipeline: bloomDownsamplePipelineHandle,
41075
+ bloomUpsamplePipeline: bloomUpsamplePipelineHandle,
38708
41076
  bloomCompositePipeline: bloomCompositePipelineHandle,
38709
- bloomBrightBindGroupLayout: bloomBrightBglHandle,
38710
- bloomBlurBindGroupLayout: bloomBlurBglHandle,
41077
+ bloomDownsampleBindGroupLayout: bloomDownsampleBglHandle,
41078
+ bloomUpsampleBindGroupLayout: bloomUpsampleBglHandle,
38711
41079
  bloomCompositeBindGroupLayout: bloomCompositeBglHandle,
38712
41080
  bloomSampler: bloomSamplerHandle,
38713
- bloomBrightParamsBuffer: bloomBrightParamsBufferHandle,
38714
- bloomBlurHParamsBuffer: bloomBlurHParamsBufferHandle,
38715
- bloomBlurVParamsBuffer: bloomBlurVParamsBufferHandle,
41081
+ bloomDownsampleParamsBuffer: bloomDownsampleParamsBufferHandle,
41082
+ bloomUpsampleParamsBuffer: bloomUpsampleParamsBufferHandle,
38716
41083
  bloomCompositeParamsBuffer: bloomCompositeParamsBufferHandle
38717
41084
  };
38718
41085
  inspectBloomResources = (graph) => {
@@ -38726,6 +41093,10 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38726
41093
  return {
38727
41094
  graphStatus: "empty",
38728
41095
  enabled: false,
41096
+ levelCount: 0,
41097
+ levelDimensions: [],
41098
+ downsamplePassCount: 0,
41099
+ upsamplePassCount: 0,
38729
41100
  targetCount: 0,
38730
41101
  targetBytes: 0,
38731
41102
  resourceCount: 0,
@@ -38749,6 +41120,10 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38749
41120
  return {
38750
41121
  graphStatus: active === void 0 ? "empty" : graphInspection.status,
38751
41122
  enabled: active !== void 0,
41123
+ levelCount: active === void 0 ? 0 : graphInspection.levelCount,
41124
+ levelDimensions: active === void 0 ? [] : graphInspection.levelDimensions,
41125
+ downsamplePassCount: active === void 0 ? 0 : graphInspection.downsamplePassCount,
41126
+ upsamplePassCount: active === void 0 ? 0 : graphInspection.upsamplePassCount,
38752
41127
  targetCount: active === void 0 ? 0 : graphInspection.targetCount,
38753
41128
  targetBytes: active === void 0 ? 0 : graphInspection.targetBytes,
38754
41129
  resourceCount: ownedBundles.reduce(
@@ -38759,7 +41134,11 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38759
41134
  encodeCount: active === void 0 ? 0 : receipts.encodeCount,
38760
41135
  bindGroupCount: active === void 0 ? 0 : receipts.bindGroupCount,
38761
41136
  uploadCount: active === void 0 ? 0 : receipts.uploadCount,
38762
- residentChildBytes: 0,
41137
+ // The graph owns the target allocations. Use its validated
41138
+ // descriptor byte sum for the active child; zero is reserved for
41139
+ // an absent/invalid current graph, never a placeholder for a
41140
+ // live Bloom allocation.
41141
+ residentChildBytes: active === void 0 || graphInspection.status !== "valid" ? 0 : graphInspection.targetBytes,
38763
41142
  generation: selected?.generation ?? 0,
38764
41143
  state
38765
41144
  };
@@ -38836,17 +41215,15 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
38836
41215
  commitBloomResources = void 0;
38837
41216
  discardBloomResources = void 0;
38838
41217
  drainBloomResources = void 0;
38839
- bloomBrightPipelineHandle = null;
38840
- bloomBlurHPipelineHandle = null;
38841
- bloomBlurVPipelineHandle = null;
41218
+ bloomDownsamplePipelineHandle = null;
41219
+ bloomUpsamplePipelineHandle = null;
38842
41220
  bloomCompositePipelineHandle = null;
38843
- bloomBrightBglHandle = null;
38844
- bloomBlurBglHandle = null;
41221
+ bloomDownsampleBglHandle = null;
41222
+ bloomUpsampleBglHandle = null;
38845
41223
  bloomCompositeBglHandle = null;
38846
41224
  bloomSamplerHandle = null;
38847
- bloomBrightParamsBufferHandle = null;
38848
- bloomBlurHParamsBufferHandle = null;
38849
- bloomBlurVParamsBufferHandle = null;
41225
+ bloomDownsampleParamsBufferHandle = null;
41226
+ bloomUpsampleParamsBufferHandle = null;
38850
41227
  bloomCompositeParamsBufferHandle = null;
38851
41228
  throw cause;
38852
41229
  }
@@ -39160,24 +41537,17 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
39160
41537
  shadowLightSpaceMatrix: null,
39161
41538
  shadowCsmLightViewProj: null,
39162
41539
  shadowCsmSelection: null,
39163
- // feat-20260531-bloom-first-declarative-render-graph-pass / w13 + w16:
39164
- // bloom per-pass resource slots. Pipeline handles assembled during
39165
- // buildReadyWebGPU (marker-triage + compile + createRenderPipeline).
39166
- // Intermediate textures are allocate in the execute closures at 1/2-res
39167
- // (ensureLazyTexture, slot width/height tracking for size-drift rebuild).
39168
- // BindGroup caches survive until the intermediate view is invalidated
39169
- // by a resize (width/height drift forces null).
39170
- bloomBrightPipeline: null,
39171
- bloomBlurHPipeline: null,
39172
- bloomBlurVPipeline: null,
41540
+ // Multiscale Bloom pipeline handles are generation-owned by the ready
41541
+ // builder; graph targets are frame-owned and are not stored here.
41542
+ bloomDownsamplePipeline: null,
41543
+ bloomUpsamplePipeline: null,
39173
41544
  bloomCompositePipeline: null,
39174
- bloomBrightBindGroupLayout: null,
39175
- bloomBlurBindGroupLayout: null,
41545
+ bloomDownsampleBindGroupLayout: null,
41546
+ bloomUpsampleBindGroupLayout: null,
39176
41547
  bloomCompositeBindGroupLayout: null,
39177
41548
  bloomSampler: null,
39178
- bloomBrightParamsBuffer: null,
39179
- bloomBlurHParamsBuffer: null,
39180
- bloomBlurVParamsBuffer: null,
41549
+ bloomDownsampleParamsBuffer: null,
41550
+ bloomUpsampleParamsBuffer: null,
39181
41551
  bloomCompositeParamsBuffer: null,
39182
41552
  ensureBloomResources: ensureBloomResourcesStable,
39183
41553
  getBloomResources: getBloomResourcesStable,
@@ -39189,18 +41559,6 @@ async function buildReadyWebGPU(rhiDevice, rendererScope, getShader, gpuStore, a
39189
41559
  retireBloomResources: retireBloomResourcesStable,
39190
41560
  drainBloomResources: drainBloomResourcesStable,
39191
41561
  inspectBloomResources: inspectBloomResourcesStable,
39192
- bloomBrightTexture: null,
39193
- bloomBrightView: null,
39194
- bloomBrightWidth: 0,
39195
- bloomBrightHeight: 0,
39196
- bloomBlurHTexture: null,
39197
- bloomBlurHView: null,
39198
- bloomBlurHWidth: 0,
39199
- bloomBlurHHeight: 0,
39200
- bloomBlurVTexture: null,
39201
- bloomBlurVView: null,
39202
- bloomBlurVWidth: 0,
39203
- bloomBlurVHeight: 0,
39204
41562
  // feat-20260612-hdrp-ssao M6 / w26 + M8 / w38: SSAO pipeline slots.
39205
41563
  ssaoCalcPipeline: ssaoCalcPipelineHandle,
39206
41564
  ssaoBlurPipeline: ssaoBlurPipelineHandle,
@@ -40246,22 +42604,13 @@ async function makeWebGPURenderer(internals) {
40246
42604
  tonemapRegistered = true;
40247
42605
  },
40248
42606
  (source) => {
40249
- if (fxaaRegistered) return;
40250
- renderSystem.registerBuiltinPostProcess(FXAA_POST_PROCESS_ID, {
40251
- source,
40252
- params: { byteSize: 16, defaultValue: new Uint8Array(16) },
40253
- reads: ["ldrColor"]
40254
- });
40255
- fxaaRegistered = true;
42607
+ fxaaRegistered = registerFxaaPostProcess(renderSystem, source, fxaaRegistered);
40256
42608
  },
42609
+ (source) => registerAnalyticFogPostProcess(renderSystem, source, candidateBuildState !== void 0),
40257
42610
  (entries) => {
40258
42611
  if (candidateBuildState !== void 0 || temporalPostProcessesRegistered) return;
40259
42612
  if (entries.motionBlur !== void 0) {
40260
- renderSystem.registerBuiltinPostProcess("forgeax.motion-blur", {
40261
- source: entries.motionBlur,
40262
- params: { byteSize: 16, defaultValue: new Uint8Array(16) },
40263
- reads: ["scene-color", "scene-temporal"]
40264
- });
42613
+ registerMotionBlurPostProcess(renderSystem, entries.motionBlur);
40265
42614
  }
40266
42615
  if (entries.taaResolve !== void 0) {
40267
42616
  renderSystem.registerBuiltinPostProcess("forgeax.taa-resolve", {
@@ -41168,20 +43517,7 @@ async function makeWebGPURenderer(internals) {
41168
43517
  const state = disposed ? "disposed" : deviceLost ? "device-lost" : health.reason === "internal-fault" || !readySettled ? "faulted" : "alive";
41169
43518
  const graph = deviceLost ? void 0 : renderSystem.perFrameGraphInfo;
41170
43519
  const standardLighting = deviceLost ? void 0 : renderSystem.standardLightingInspection;
41171
- const bloom = deviceLost ? {
41172
- graphStatus: "empty",
41173
- enabled: false,
41174
- targetCount: 0,
41175
- targetBytes: 0,
41176
- resourceCount: 0,
41177
- passCount: 0,
41178
- encodeCount: 0,
41179
- bindGroupCount: 0,
41180
- uploadCount: 0,
41181
- residentChildBytes: 0,
41182
- generation: 0,
41183
- state: "off"
41184
- } : renderSystem.bloom;
43520
+ const bloom = deviceLost ? emptyBloomInspection() : renderSystem.bloom;
41185
43521
  const surfaceStorage = pipelineState?.format ?? "rgba8unorm";
41186
43522
  const surfaceDisplay = pipelineState?.colorAttachmentFormat ?? "rgba8unorm";
41187
43523
  const observationId = `${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}:frame-${frameId}`;
@@ -41600,6 +43936,7 @@ async function makeWebGPURenderer(internals) {
41600
43936
  cameraOwner,
41601
43937
  resourceOwner,
41602
43938
  ...frameRequest.camera.entityKey === void 0 ? {} : { cameraEntityKey: frameRequest.camera.entityKey },
43939
+ ...motionBlurDrawOptions(frameRequest),
41603
43940
  ...frameRequest.profileFrame === void 0 ? {} : { profileFrame: frameRequest.profileFrame }
41604
43941
  } : options ?? { cameraOwner: 0, resourceOwner: 0 };
41605
43942
  if (disposed) {
@@ -41691,6 +44028,8 @@ async function makeWebGPURenderer(internals) {
41691
44028
  const cleanupFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
41692
44029
  volumeTimingCapture?.discard();
41693
44030
  if (cleanupFailure !== void 0) return err(cleanupFailure);
44031
+ const invalidMotionBlur = renderSystem.motionBlurInvalidParams;
44032
+ if (invalidMotionBlur !== void 0) return err(invalidMotionBlur);
41694
44033
  return err(
41695
44034
  new RendererContractFailureError(
41696
44035
  "draw",
@@ -41981,7 +44320,7 @@ async function makeWebGPURenderer(internals) {
41981
44320
  receiptObservationBuffers.delete(receipt);
41982
44321
  }
41983
44322
  }
41984
- await renderSystem.observeLodOcclusion();
44323
+ await observeLodOcclusionForReceipt(renderSystem, receipt);
41985
44324
  const targetReadbacks = request.targetReadbacks === void 0 || request.targetReadbacks.length === 0 ? void 0 : await renderTargetHost.observeTargetReadbacks(receipt, request.targetReadbacks);
41986
44325
  if (targetReadbacks !== void 0 && !targetReadbacks.ok) return targetReadbacks;
41987
44326
  const volumeTimings = request.include.includes("timings") ? await (receiptTimings.get(receipt) ?? Promise.resolve({