@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
@@ -5,6 +5,7 @@ import type {
5
5
  } from '@forgeax/engine-render-graph';
6
6
  import { RenderGraphError } from '@forgeax/engine-render-graph';
7
7
  import { err, ok, type Result } from '@forgeax/engine-types';
8
+ import { standardBloomAdmitted } from '../bloom-admission';
8
9
  import { addTypedDebugOverlayPass } from '../debug-draw-glue';
9
10
  import { SceneDataUnavailableError } from '../errors/render';
10
11
  import { addDepthOfFieldPasses } from '../features/depth-of-field/depth-of-field-feature';
@@ -30,6 +31,7 @@ import {
30
31
  TYPED_BLOOM_PASS_NAMES,
31
32
  type TypedTemporalResolveTargets,
32
33
  } from '../typed-render-graph-primitives';
34
+ import { renderExtentSize } from './render-extent';
33
35
  import {
34
36
  addAutoExposureExposurePass,
35
37
  addAutoExposureGraphPasses,
@@ -42,22 +44,81 @@ interface StandardPostSurface {
42
44
  readonly storage: RenderPipelineTarget;
43
45
  }
44
46
 
47
+ export const STANDARD_BLOOM_MAX_LEVELS = 5;
48
+
45
49
  /** One authored Bloom contract drives declaration and compiled-graph validation. */
46
50
  export const STANDARD_BLOOM_TARGET_SPEC = {
47
- composited: { label: 'bloom-composited', format: 'rgba16float', size: 'surface' },
48
- bright: { label: 'bloom-bright', format: 'rgba16float', size: 'half-surface' },
49
- blurH: { label: 'bloom-blur-h', format: 'rgba16float', size: 'half-surface' },
50
- blurV: { label: 'bloom-blur-v', format: 'rgba16float', size: 'half-surface' },
51
- } as const satisfies Record<
52
- 'composited' | 'bright' | 'blurH' | 'blurV',
53
- Pick<GraphTextureDescriptor, 'format' | 'size'> & { readonly label: string }
54
- >;
51
+ composited: { label: 'bloom-composited', format: 'rgba16float' },
52
+ downsamplePrefix: 'bloom-downsample-',
53
+ upsamplePrefix: 'bloom-upsample-',
54
+ } as const;
55
+
56
+ export interface StandardBloomLevelDimensions {
57
+ readonly width: number;
58
+ readonly height: number;
59
+ }
60
+
61
+ /** Derive the bounded ceil-halving output pyramid, including D0. */
62
+ export function deriveStandardBloomLevels(
63
+ outputWidth: number,
64
+ outputHeight: number,
65
+ ): readonly StandardBloomLevelDimensions[] {
66
+ const levels: StandardBloomLevelDimensions[] = [];
67
+ let width = Math.max(1, Math.ceil(outputWidth / 2));
68
+ let height = Math.max(1, Math.ceil(outputHeight / 2));
69
+ for (let level = 0; level < STANDARD_BLOOM_MAX_LEVELS; level += 1) {
70
+ levels.push({ width, height });
71
+ if (width === 1 && height === 1) break;
72
+ width = Math.max(1, Math.ceil(width / 2));
73
+ height = Math.max(1, Math.ceil(height / 2));
74
+ }
75
+ return levels;
76
+ }
77
+
78
+ export interface StandardBloomTargetSpecs {
79
+ readonly composited: GraphTextureDescriptor & { readonly label: string };
80
+ readonly downsample: readonly (GraphTextureDescriptor & { readonly label: string })[];
81
+ readonly upsample: readonly (GraphTextureDescriptor & { readonly label: string })[];
82
+ readonly levels: readonly StandardBloomLevelDimensions[];
83
+ }
84
+
85
+ function standardBloomTargetSpecs(
86
+ outputWidth: number,
87
+ outputHeight: number,
88
+ ): StandardBloomTargetSpecs {
89
+ const levels = deriveStandardBloomLevels(outputWidth, outputHeight);
90
+ return {
91
+ composited: {
92
+ label: STANDARD_BLOOM_TARGET_SPEC.composited.label,
93
+ format: STANDARD_BLOOM_TARGET_SPEC.composited.format,
94
+ size: { width: outputWidth, height: outputHeight },
95
+ domain: 'linear-hdr',
96
+ },
97
+ downsample: levels.map((size, level) => ({
98
+ label: `${STANDARD_BLOOM_TARGET_SPEC.downsamplePrefix}${level}`,
99
+ format: 'rgba16float' as const,
100
+ size,
101
+ domain: 'linear-hdr' as const,
102
+ })),
103
+ upsample: levels.slice(0, -1).map((size, level) => ({
104
+ label: `${STANDARD_BLOOM_TARGET_SPEC.upsamplePrefix}${level}`,
105
+ format: 'rgba16float' as const,
106
+ size,
107
+ domain: 'linear-hdr' as const,
108
+ })),
109
+ levels,
110
+ };
111
+ }
55
112
 
56
113
  export interface StandardBloomGraphInspection {
57
114
  readonly status: 'empty' | 'valid' | 'invalid';
58
115
  readonly targetCount: number;
59
116
  readonly targetBytes: number;
60
117
  readonly passCount: number;
118
+ readonly levelCount: number;
119
+ readonly levelDimensions: readonly StandardBloomLevelDimensions[];
120
+ readonly downsamplePassCount: number;
121
+ readonly upsamplePassCount: number;
61
122
  }
62
123
 
63
124
  const emptyBloomGraphInspection = (): StandardBloomGraphInspection => ({
@@ -65,6 +126,10 @@ const emptyBloomGraphInspection = (): StandardBloomGraphInspection => ({
65
126
  targetCount: 0,
66
127
  targetBytes: 0,
67
128
  passCount: 0,
129
+ levelCount: 0,
130
+ levelDimensions: [],
131
+ downsamplePassCount: 0,
132
+ upsamplePassCount: 0,
68
133
  });
69
134
 
70
135
  const invalidBloomGraphInspection = (): StandardBloomGraphInspection => ({
@@ -72,21 +137,23 @@ const invalidBloomGraphInspection = (): StandardBloomGraphInspection => ({
72
137
  targetCount: 0,
73
138
  targetBytes: 0,
74
139
  passCount: 0,
140
+ levelCount: 0,
141
+ levelDimensions: [],
142
+ downsamplePassCount: 0,
143
+ upsamplePassCount: 0,
75
144
  });
76
145
 
77
- function textureBytes(
78
- resource: CompiledRenderGraphInfo['resources'][number],
79
- expected: (typeof STANDARD_BLOOM_TARGET_SPEC)[keyof typeof STANDARD_BLOOM_TARGET_SPEC],
80
- ): number {
146
+ type CompiledBloomResource = CompiledRenderGraphInfo['resources'][number];
147
+
148
+ function textureBytes(resource: CompiledBloomResource): number | undefined {
81
149
  const descriptor = resource.descriptor;
82
150
  if (
151
+ resource.kind !== 'texture' ||
83
152
  descriptor === undefined ||
84
153
  descriptor.kind !== 'texture' ||
85
- descriptor.format !== expected.format ||
86
- !('size' in descriptor) ||
87
- descriptor.size !== expected.size
154
+ descriptor.format !== 'rgba16float'
88
155
  )
89
- return 0;
156
+ return undefined;
90
157
  const texelBytes = 8;
91
158
  let bytes = 0;
92
159
  for (let level = 0; level < descriptor.mipLevelCount; level += 1) {
@@ -96,7 +163,56 @@ function textureBytes(
96
163
  descriptor.depthOrArrayLayers *
97
164
  texelBytes;
98
165
  }
99
- return bytes * descriptor.sampleCount;
166
+ const descriptorBytes = bytes * descriptor.sampleCount;
167
+ return resource.byteSize === descriptorBytes && descriptorBytes > 0 ? descriptorBytes : undefined;
168
+ }
169
+
170
+ function resourceSize(
171
+ resource: CompiledBloomResource,
172
+ ): { readonly width: number; readonly height: number } | undefined {
173
+ const descriptor = resource.descriptor;
174
+ if (resource.kind !== 'texture' || descriptor.kind !== 'texture') return undefined;
175
+ if (typeof descriptor.size === 'string') return undefined;
176
+ if (
177
+ resource.extent === undefined ||
178
+ resource.extent.width !== descriptor.width ||
179
+ resource.extent.height !== descriptor.height ||
180
+ resource.extent.depthOrArrayLayers !== descriptor.depthOrArrayLayers
181
+ ) {
182
+ return undefined;
183
+ }
184
+ if (
185
+ descriptor.format !== 'rgba16float' ||
186
+ descriptor.domain !== 'linear-hdr' ||
187
+ descriptor.width <= 0 ||
188
+ descriptor.height <= 0 ||
189
+ descriptor.depthOrArrayLayers !== 1 ||
190
+ descriptor.mipLevelCount !== 1 ||
191
+ descriptor.sampleCount !== 1 ||
192
+ resource.dimension !== '2d' ||
193
+ descriptor.size.width !== descriptor.width ||
194
+ descriptor.size.height !== descriptor.height ||
195
+ textureBytes(resource) === undefined
196
+ ) {
197
+ return undefined;
198
+ }
199
+ return { width: descriptor.width, height: descriptor.height };
200
+ }
201
+
202
+ function hasPassAccesses(
203
+ pass: CompiledRenderGraphInfo['passes'][number] | undefined,
204
+ accesses: readonly { readonly resource: string; readonly usage: string }[],
205
+ ): boolean {
206
+ return (
207
+ pass !== undefined &&
208
+ pass.kind === 'raster' &&
209
+ pass.accesses.length === accesses.length &&
210
+ accesses.every(
211
+ (access, index) =>
212
+ pass.accesses[index]?.resource === access.resource &&
213
+ pass.accesses[index]?.usage === access.usage,
214
+ )
215
+ );
100
216
  }
101
217
 
102
218
  /** Derive Bloom target/pass facts solely from the compiled Standard graph. */
@@ -104,33 +220,125 @@ export function inspectStandardBloomGraph(
104
220
  graph: CompiledRenderGraphInfo | undefined,
105
221
  ): StandardBloomGraphInspection {
106
222
  if (graph === undefined) return emptyBloomGraphInspection();
107
- type BloomTargetSpec =
108
- (typeof STANDARD_BLOOM_TARGET_SPEC)[keyof typeof STANDARD_BLOOM_TARGET_SPEC];
109
- const targetSpec = new Map<string, BloomTargetSpec>(
110
- Object.values(STANDARD_BLOOM_TARGET_SPEC).map((entry) => [entry.label, entry] as const),
223
+ const composited = graph.resources.filter(
224
+ (resource) => resource.label === STANDARD_BLOOM_TARGET_SPEC.composited.label,
111
225
  );
112
- const targets = graph.resources.flatMap((resource) => {
113
- const expected = targetSpec.get(resource.label);
114
- return expected === undefined ? [] : [{ resource, expected }];
115
- });
116
- const passNames = new Set<string>(Object.values(TYPED_BLOOM_PASS_NAMES));
117
- const passes = graph.passes.filter((pass) => passNames.has(pass.name));
226
+ const downsample = graph.resources
227
+ .filter((resource) => resource.label.startsWith(STANDARD_BLOOM_TARGET_SPEC.downsamplePrefix))
228
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
229
+ const upsample = graph.resources
230
+ .filter((resource) => resource.label.startsWith(STANDARD_BLOOM_TARGET_SPEC.upsamplePrefix))
231
+ .sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
232
+ const targets = [...composited, ...downsample, ...upsample];
233
+ const passes = graph.passes
234
+ .filter(
235
+ (pass) =>
236
+ pass.name === TYPED_BLOOM_PASS_NAMES.composite ||
237
+ pass.name.startsWith(`${TYPED_BLOOM_PASS_NAMES.downsample}-`) ||
238
+ pass.name.startsWith(`${TYPED_BLOOM_PASS_NAMES.upsample}-`),
239
+ )
240
+ .sort((left, right) => left.executionIndex - right.executionIndex);
118
241
  if (targets.length === 0 && passes.length === 0) return emptyBloomGraphInspection();
242
+ if (composited.length !== 1) return invalidBloomGraphInspection();
243
+ const compositedResource = composited[0];
244
+ if (compositedResource === undefined) return invalidBloomGraphInspection();
245
+ const compositedSize = resourceSize(compositedResource);
246
+ if (compositedSize === undefined) return invalidBloomGraphInspection();
247
+
248
+ const expected = standardBloomTargetSpecs(compositedSize.width, compositedSize.height);
249
+ const expectedDownsampleLabels = expected.downsample.map((entry) => entry.label);
250
+ const expectedUpsampleLabels = expected.upsample.map((entry) => entry.label);
251
+ const orderedDownsample = downsample.sort((left, right) =>
252
+ left.label.localeCompare(right.label, undefined, { numeric: true }),
253
+ );
254
+ const orderedUpsample = upsample.sort((left, right) =>
255
+ left.label.localeCompare(right.label, undefined, { numeric: true }),
256
+ );
119
257
  const completeTargetSet =
120
- targets.length === Object.values(STANDARD_BLOOM_TARGET_SPEC).length &&
121
- targets.every(({ resource, expected }) => textureBytes(resource, expected) > 0);
122
- const completePassSet = passes.length === Object.values(TYPED_BLOOM_PASS_NAMES).length;
123
- if (!completeTargetSet || !completePassSet) {
258
+ targets.length === 1 + expectedDownsampleLabels.length + expectedUpsampleLabels.length &&
259
+ orderedDownsample.length === expectedDownsampleLabels.length &&
260
+ orderedUpsample.length === expectedUpsampleLabels.length &&
261
+ orderedDownsample.every(
262
+ (resource, level) => resource.label === expectedDownsampleLabels[level],
263
+ ) &&
264
+ orderedUpsample.every((resource, level) => resource.label === expectedUpsampleLabels[level]) &&
265
+ orderedDownsample.every(
266
+ (resource, level) =>
267
+ resourceSize(resource)?.width === expected.levels[level]?.width &&
268
+ resourceSize(resource)?.height === expected.levels[level]?.height,
269
+ ) &&
270
+ orderedUpsample.every(
271
+ (resource, level) =>
272
+ resourceSize(resource)?.width === expected.levels[level]?.width &&
273
+ resourceSize(resource)?.height === expected.levels[level]?.height,
274
+ );
275
+ const expectedPassNames = [
276
+ ...expectedDownsampleLabels,
277
+ ...[...expectedUpsampleLabels].reverse(),
278
+ TYPED_BLOOM_PASS_NAMES.composite,
279
+ ];
280
+ const completePassSet =
281
+ passes.length === expected.levels.length + expected.upsample.length + 1 &&
282
+ passes.every((pass, index) => pass.name === expectedPassNames[index]);
283
+ const sceneLabel = passes[0]?.accesses[0]?.resource;
284
+ const passTopology =
285
+ completePassSet &&
286
+ sceneLabel !== undefined &&
287
+ !sceneLabel.startsWith(STANDARD_BLOOM_TARGET_SPEC.downsamplePrefix) &&
288
+ !sceneLabel.startsWith(STANDARD_BLOOM_TARGET_SPEC.upsamplePrefix) &&
289
+ sceneLabel !== STANDARD_BLOOM_TARGET_SPEC.composited.label &&
290
+ graph.resources.some((resource) => resource.label === sceneLabel) &&
291
+ orderedDownsample.every((_, level) =>
292
+ hasPassAccesses(passes[level], [
293
+ {
294
+ resource: level === 0 ? sceneLabel : `bloom-downsample-${level - 1}`,
295
+ usage: 'sampled-read',
296
+ },
297
+ { resource: `bloom-downsample-${level}`, usage: 'color-attachment' },
298
+ ]),
299
+ ) &&
300
+ orderedUpsample.every((_, level) => {
301
+ const passIndex = expected.levels.length + expected.upsample.length - 1 - level;
302
+ return hasPassAccesses(passes[passIndex], [
303
+ { resource: `bloom-downsample-${level}`, usage: 'sampled-read' },
304
+ {
305
+ resource:
306
+ level === expected.levels.length - 2
307
+ ? `bloom-downsample-${level + 1}`
308
+ : `bloom-upsample-${level + 1}`,
309
+ usage: 'sampled-read',
310
+ },
311
+ { resource: `bloom-upsample-${level}`, usage: 'color-attachment' },
312
+ ]);
313
+ }) &&
314
+ hasPassAccesses(passes.at(-1), [
315
+ { resource: sceneLabel, usage: 'sampled-read' },
316
+ {
317
+ resource: expected.levels.length === 1 ? 'bloom-downsample-0' : 'bloom-upsample-0',
318
+ usage: 'sampled-read',
319
+ },
320
+ { resource: STANDARD_BLOOM_TARGET_SPEC.composited.label, usage: 'color-attachment' },
321
+ ]);
322
+ if (!completeTargetSet || !passTopology) {
124
323
  return invalidBloomGraphInspection();
125
324
  }
325
+ const levelDimensions = orderedDownsample.flatMap((resource) => {
326
+ const size = resourceSize(resource);
327
+ return size === undefined ? [] : [size];
328
+ });
329
+ if (levelDimensions.length !== orderedDownsample.length) {
330
+ return invalidBloomGraphInspection();
331
+ }
332
+ const targetBytes = targets.reduce((bytes, resource) => bytes + (textureBytes(resource) ?? 0), 0);
126
333
  return {
127
334
  status: 'valid',
128
335
  targetCount: targets.length,
129
- targetBytes: targets.reduce(
130
- (bytes, target) => bytes + textureBytes(target.resource, target.expected),
131
- 0,
132
- ),
336
+ targetBytes,
133
337
  passCount: passes.length,
338
+ levelCount: orderedDownsample.length,
339
+ levelDimensions,
340
+ downsamplePassCount: orderedDownsample.length,
341
+ upsamplePassCount: orderedUpsample.length,
134
342
  };
135
343
  }
136
344
 
@@ -160,8 +368,8 @@ function missingTemporalContributor(
160
368
  /**
161
369
  * Build the one Standard post chain shared by direct and clustered lighting.
162
370
  * Bloom is admitted before any Bloom target is declared, keeping the disabled
163
- * path exact-zero while preserving the existing half-resolution four-pass
164
- * implementation for the enabled path.
371
+ * path exact-zero while building one bounded five-level HDR
372
+ * downsample/tent-upsample pyramid for the enabled path.
165
373
  */
166
374
  export function addStandardPost(
167
375
  context: RenderPipelineBuildContext<RenderPipelineFrame>,
@@ -179,7 +387,7 @@ export function addStandardPost(
179
387
  lane: topology.standardProfile?.renderPath === 'deferred' ? 'clustered' : 'direct',
180
388
  temporal: !clearOnly && topology.camera.antialias === 'taa',
181
389
  meter: topology.output?.autoExposure === true,
182
- bloom: !clearOnly && topology.camera.bloom === 'on' && topology.camera.tonemap !== 'none',
390
+ bloom: !clearOnly && standardBloomAdmitted(topology.camera),
183
391
  dof: !clearOnly && topology.camera.depthOfField !== undefined,
184
392
  exposure: topology.output?.autoExposure === true,
185
393
  whiteBalance: topology.output?.whiteBalance === true,
@@ -220,6 +428,33 @@ export function addStandardPost(
220
428
  const finalPresent = surface.storage;
221
429
  let postInput = scene;
222
430
  let postTemporal = sceneTemporal;
431
+ if (!clearOnly && topology.analyticFog === true) {
432
+ if (depth.sampleCount !== 1)
433
+ return err(
434
+ new RenderGraphError({
435
+ code: 'resource-resolution-failed',
436
+ expected: 'analytic fog receives single-sample scene depth',
437
+ hint: 'select FXAA, TAA or no AA for analytic fog',
438
+ detail: { resourceLabel: 'scene-depth' },
439
+ }),
440
+ );
441
+ const fogged = target(context, 'analytic-fog-color', {
442
+ format: 'rgba16float',
443
+ size:
444
+ topology.extent === undefined ? 'surface' : renderExtentSize(topology.extent, 'internal'),
445
+ domain: hdr ? 'linear-hdr' : 'linear-ldr',
446
+ });
447
+ if (!fogged.ok) return fogged;
448
+ const fog = addTypedFullscreenPass(context.graph, {
449
+ name: 'analytic-fog',
450
+ shader: 'forgeax.analytic-fog',
451
+ input: postInput,
452
+ depth,
453
+ output: fogged.value,
454
+ });
455
+ if (!fog.ok) return fog;
456
+ postInput = fogged.value;
457
+ }
223
458
 
224
459
  const autoExposure = context.standardOutput?.autoExposure;
225
460
  if (outputPlan.features.exposure && autoExposure === undefined) {
@@ -241,7 +476,7 @@ export function addStandardPost(
241
476
  return err(missingTemporalContributor(topology, 'forgeax::standard::taa-history'));
242
477
  }
243
478
  const resolveTargets: TypedTemporalResolveTargets = {
244
- scene,
479
+ scene: postInput,
245
480
  depth,
246
481
  currentTemporal: sceneTemporal,
247
482
  ...(coverage === undefined ? {} : { coverage }),
@@ -262,6 +497,20 @@ export function addStandardPost(
262
497
  postTemporal = history.currentTemporal;
263
498
  }
264
499
 
500
+ // Meter the temporally resolved color before lens-domain filters. Motion
501
+ // Blur and Bloom must consume a single exposure decision for this frame;
502
+ // running the meter after either filter makes the measured luminance depend
503
+ // on the authored streak radius and changes the exposure feedback loop.
504
+ if (outputPlan.features.exposure && autoExposure !== undefined) {
505
+ const added = addAutoExposureGraphPasses(context, {
506
+ source: postInput,
507
+ resources: autoExposure,
508
+ width: topology.surface.width,
509
+ height: topology.surface.height,
510
+ });
511
+ if (!added.ok) return added;
512
+ }
513
+
265
514
  // Feature projection and the built-in fallback share one target contract.
266
515
  // The projection runs after TAA so Motion Blur consumes the resolved color,
267
516
  // then both paths feed the same linear float target into Bloom/Output.
@@ -315,18 +564,6 @@ export function addStandardPost(
315
564
  }
316
565
  }
317
566
 
318
- // Meter the temporally resolved color before DoF/Bloom. This preserves the
319
- // Standard order: temporal -> motion -> meter -> DoF -> bloom -> output.
320
- if (outputPlan.features.exposure && autoExposure !== undefined) {
321
- const added = addAutoExposureGraphPasses(context, {
322
- source: postInput,
323
- resources: autoExposure,
324
- width: topology.surface.width,
325
- height: topology.surface.height,
326
- });
327
- if (!added.ok) return added;
328
- }
329
-
330
567
  if (outputPlan.features.dof) {
331
568
  const dof = addDepthOfFieldPasses(
332
569
  {
@@ -343,38 +580,30 @@ export function addStandardPost(
343
580
  if (!dof.ok) return dof;
344
581
  postInput = dof.value;
345
582
  }
346
-
347
583
  if (bloomActive) {
348
- const composited = target(
349
- context,
350
- STANDARD_BLOOM_TARGET_SPEC.composited.label,
351
- STANDARD_BLOOM_TARGET_SPEC.composited,
352
- );
584
+ const outputWidth = topology.extent?.outputWidth ?? topology.surface.width;
585
+ const outputHeight = topology.extent?.outputHeight ?? topology.surface.height;
586
+ const bloomSpecs = standardBloomTargetSpecs(outputWidth, outputHeight);
587
+ const composited = target(context, bloomSpecs.composited.label, bloomSpecs.composited);
353
588
  if (!composited.ok) return composited;
354
- const bright = target(
355
- context,
356
- STANDARD_BLOOM_TARGET_SPEC.bright.label,
357
- STANDARD_BLOOM_TARGET_SPEC.bright,
358
- );
359
- if (!bright.ok) return bright;
360
- const blurH = target(
361
- context,
362
- STANDARD_BLOOM_TARGET_SPEC.blurH.label,
363
- STANDARD_BLOOM_TARGET_SPEC.blurH,
364
- );
365
- if (!blurH.ok) return blurH;
366
- const blurV = target(
367
- context,
368
- STANDARD_BLOOM_TARGET_SPEC.blurV.label,
369
- STANDARD_BLOOM_TARGET_SPEC.blurV,
370
- );
371
- if (!blurV.ok) return blurV;
589
+ const downsample: RenderPipelineTarget[] = [];
590
+ for (const spec of bloomSpecs.downsample) {
591
+ const level = target(context, spec.label, spec);
592
+ if (!level.ok) return level;
593
+ downsample.push(level.value);
594
+ }
595
+ const upsample: RenderPipelineTarget[] = [];
596
+ for (const spec of bloomSpecs.upsample) {
597
+ const level = target(context, spec.label, spec);
598
+ if (!level.ok) return level;
599
+ upsample.push(level.value);
600
+ }
372
601
  const bloom = addTypedBloomPasses(context.graph, {
373
602
  scene: postInput,
374
603
  composited: composited.value,
375
- bright: bright.value,
376
- blurH: blurH.value,
377
- blurV: blurV.value,
604
+ downsample,
605
+ upsample,
606
+ levelDimensions: bloomSpecs.levels,
378
607
  });
379
608
  if (!bloom.ok) return bloom;
380
609
  postInput = composited.value;
@@ -562,8 +562,8 @@ export interface PassKindAttachmentPolicy {
562
562
  * 4. `'point-shadow-caster'` — HDRP point-shadow caster: depth-only
563
563
  * 5. `'skybox'` — fullscreen skybox: color-only clear/store
564
564
  * 6. `'tonemap'` — HDR→LDR tonemap fullscreen: color-only clear/store
565
- * 7. `'bloom-bright'` / `'bloom-blur'` — bloom downsample/blur: color-only
566
- * clear/store
565
+ * 7. `'bloom-downsample'` / `'bloom-upsample'` — multiscale Bloom pyramid:
566
+ * color-only clear/store
567
567
  * 8. `'bloom-composite'` — bloom add-back: color-only load/store (NOT clear)
568
568
  * 9. `'fxaa'` — fullscreen FXAA: color-only clear/store
569
569
  * 10. `'post-process'` — generic fullscreen primitive (SSAO, render-graph
@@ -633,7 +633,7 @@ export const passKindPolicyTable: Readonly<Record<string, PassKindAttachmentPoli
633
633
  },
634
634
  defaultDepthOps: undefined,
635
635
  },
636
- 'bloom-bright': {
636
+ 'bloom-downsample': {
637
637
  shape: 'color-only',
638
638
  defaultColorOps: {
639
639
  loadOp: 'clear',
@@ -642,7 +642,7 @@ export const passKindPolicyTable: Readonly<Record<string, PassKindAttachmentPoli
642
642
  },
643
643
  defaultDepthOps: undefined,
644
644
  },
645
- 'bloom-blur': {
645
+ 'bloom-upsample': {
646
646
  shape: 'color-only',
647
647
  defaultColorOps: {
648
648
  loadOp: 'clear',
@@ -1220,7 +1220,7 @@ export function buildSpecConstTable(
1220
1220
  // has sampleCount=4. Both use empty vertexLayout (fullscreen triangle, no
1221
1221
  // attributes) and cullMode='none' (forward cull for fullscreen-quad passthrough).
1222
1222
  //
1223
- // Shadow-probe / fxaa / bloom 4-stage / SSAO 2-stage are lazy-build — not in
1223
+ // Shadow-probe / fxaa / Bloom 10-pass / SSAO 2-stage are lazy-build — not in
1224
1224
  // SPEC_CONST_TABLE (R-D4 decision: only entries that are always-present on
1225
1225
  // every boot).
1226
1226
 
@@ -25,7 +25,8 @@ function camera(): CameraSnapshot {
25
25
  bloom: 'off',
26
26
  bloomThreshold: 1,
27
27
  bloomIntensity: 1,
28
- bloomBlurRadius: 4,
28
+ bloomSoftKnee: 0.5,
29
+ bloomScatter: 0.7,
29
30
  clearColor: [0, 0, 0, 1],
30
31
  };
31
32
  }
@@ -70,7 +70,8 @@ function camera(): CameraSnapshot {
70
70
  bloom: 'off',
71
71
  bloomThreshold: 1,
72
72
  bloomIntensity: 1,
73
- bloomBlurRadius: 4,
73
+ bloomSoftKnee: 0.5,
74
+ bloomScatter: 0.7,
74
75
  clearColor: [0, 0, 0, 1],
75
76
  };
76
77
  }
@@ -22,6 +22,7 @@ import type { EnvironmentGeneration } from '../environment/generation';
22
22
  import type { EnvironmentLifecycle } from '../environment/lifecycle';
23
23
  import type { DepthOfFieldParams } from '../features/depth-of-field/depth-of-field-params';
24
24
  import type { GpuBuffer } from '../gpu-resource';
25
+ import type { MotionBlurExecutionReceipt } from '../inspection-types';
25
26
  import type { InstanceCollectionStore } from '../instances';
26
27
  import type { IblBindingInspection } from '../mesh-material-bindings';
27
28
  import type { StandardLightingInspection } from '../pipeline/standard-lighting/inspection';
@@ -382,7 +383,8 @@ export function makeZeroCameraFallbackSnapshot(): CameraSnapshot {
382
383
  bloom: 'off',
383
384
  bloomThreshold: 1.0,
384
385
  bloomIntensity: 1.0,
385
- bloomBlurRadius: 4.0,
386
+ bloomSoftKnee: 0.5,
387
+ bloomScatter: 0.7,
386
388
  clearColor: ZERO_CAMERA_CLEAR_FALLBACK,
387
389
  };
388
390
  }
@@ -407,6 +409,8 @@ export function makeZeroCameraFallbackSnapshot(): CameraSnapshot {
407
409
  */
408
410
  export interface RenderFrameState {
409
411
  frameNumber: number;
412
+ /** Last successful Motion Blur compute execution; failed submits retain the prior receipt. */
413
+ motionBlurExecution?: MotionBlurExecutionReceipt;
410
414
  /** Final-submit Standard IBL binding-chain receipt for this frame. */
411
415
  iblBindingInspection?: IblBindingInspection;
412
416
  /** Demand for the frame-local Standard fallback MRT. */
@@ -18,6 +18,12 @@ export function graphExecutionPhase(passName: string): RenderGraphExecutionPhase
18
18
  return 'record/graph-execute/ssr-reflection-mip';
19
19
  }
20
20
  if (passName === 'ssr-compose') return 'record/graph-execute/ssr-compose';
21
+ if (passName.startsWith('bloom-downsample-')) {
22
+ return 'record/graph-execute/bloom-downsample';
23
+ }
24
+ if (passName.startsWith('bloom-upsample-')) {
25
+ return 'record/graph-execute/bloom-upsample';
26
+ }
21
27
  switch (passName) {
22
28
  case 'cluster-binner-upload':
23
29
  case 'cluster-membership-producer':
@@ -47,12 +53,10 @@ export function graphExecutionPhase(passName: string): RenderGraphExecutionPhase
47
53
  return 'record/graph-execute/main';
48
54
  case 'fxaa':
49
55
  return 'record/graph-execute/fxaa';
50
- case 'bloom-bright':
51
- return 'record/graph-execute/bloom-bright';
52
- case 'bloom-blur-h':
53
- return 'record/graph-execute/bloom-blur-h';
54
- case 'bloom-blur-v':
55
- return 'record/graph-execute/bloom-blur-v';
56
+ case 'bloom-downsample':
57
+ return 'record/graph-execute/bloom-downsample';
58
+ case 'bloom-upsample':
59
+ return 'record/graph-execute/bloom-upsample';
56
60
  case 'bloom-composite':
57
61
  return 'record/graph-execute/bloom-composite';
58
62
  default: