@forgeax/engine-render 0.1.30 → 0.1.32

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 (118) hide show
  1. package/README.md +68 -1
  2. package/dist/assembly/webgpu-ready.d.ts +2 -1
  3. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  4. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  5. package/dist/{chunk-C3DDSXYK.mjs → chunk-4NNZQCL3.mjs} +110 -35
  6. package/dist/chunk-4NNZQCL3.mjs.map +1 -0
  7. package/dist/{chunk-JRBDO2KB.mjs → chunk-6TFBXLRJ.mjs} +19 -7
  8. package/dist/chunk-6TFBXLRJ.mjs.map +1 -0
  9. package/dist/{chunk-FUUO4UND.mjs → chunk-DRR7D3KX.mjs} +3 -3
  10. package/dist/chunk-DRR7D3KX.mjs.map +1 -0
  11. package/dist/{chunk-S5UE2T5P.mjs → chunk-GWCEMDVR.mjs} +380 -4
  12. package/dist/chunk-GWCEMDVR.mjs.map +1 -0
  13. package/dist/{chunk-MDQJPY3T.mjs → chunk-JCIRYQ4V.mjs} +3 -3
  14. package/dist/{chunk-MDQJPY3T.mjs.map → chunk-JCIRYQ4V.mjs.map} +1 -1
  15. package/dist/{chunk-PSEQ6QPH.mjs → chunk-JRIPCMYC.mjs} +17 -10
  16. package/dist/chunk-JRIPCMYC.mjs.map +1 -0
  17. package/dist/{chunk-7MQ3XBSP.mjs → chunk-LULCA3OF.mjs} +4 -4
  18. package/dist/{chunk-7MQ3XBSP.mjs.map → chunk-LULCA3OF.mjs.map} +1 -1
  19. package/dist/{chunk-NO4WZCAH.mjs → chunk-S665NJNF.mjs} +3 -3
  20. package/dist/{chunk-NO4WZCAH.mjs.map → chunk-S665NJNF.mjs.map} +1 -1
  21. package/dist/components/depth-of-field.d.ts +28 -0
  22. package/dist/components/depth-of-field.d.ts.map +1 -0
  23. package/dist/components/directional-light.d.ts +2 -1
  24. package/dist/components/directional-light.d.ts.map +1 -1
  25. package/dist/components/index.d.ts +1 -0
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/construct-renderer.mjs +898 -328
  28. package/dist/construct-renderer.mjs.map +1 -1
  29. package/dist/extract/camera.d.ts.map +1 -1
  30. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts +19 -0
  31. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts.map +1 -0
  32. package/dist/features/depth-of-field/depth-of-field-feature.d.ts +43 -0
  33. package/dist/features/depth-of-field/depth-of-field-feature.d.ts.map +1 -0
  34. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts +34 -0
  35. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts.map +1 -0
  36. package/dist/features/depth-of-field/depth-of-field-params.d.ts +83 -0
  37. package/dist/features/depth-of-field/depth-of-field-params.d.ts.map +1 -0
  38. package/dist/features/types.d.ts +19 -1
  39. package/dist/features/types.d.ts.map +1 -1
  40. package/dist/index.d.ts +4 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.mjs +6 -5
  43. package/dist/index.mjs.map +1 -1
  44. package/dist/inspection-types.d.ts +35 -0
  45. package/dist/inspection-types.d.ts.map +1 -1
  46. package/dist/instances.d.ts +7 -0
  47. package/dist/instances.d.ts.map +1 -1
  48. package/dist/internal.mjs +6 -6
  49. package/dist/mesh-material-bindings.d.ts +3 -0
  50. package/dist/mesh-material-bindings.d.ts.map +1 -1
  51. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  52. package/dist/pipeline/standard-output/types.d.ts +4 -2
  53. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  54. package/dist/pipeline/standard-post.d.ts.map +1 -1
  55. package/dist/plugin.d.ts.map +1 -1
  56. package/dist/record/frame-snapshot.d.ts +21 -1
  57. package/dist/record/frame-snapshot.d.ts.map +1 -1
  58. package/dist/record/frame.d.ts.map +1 -1
  59. package/dist/record/main-pass.d.ts.map +1 -1
  60. package/dist/record/typed-frame-graph.d.ts +7 -0
  61. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  62. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  63. package/dist/render-contract.d.ts +8 -1
  64. package/dist/render-contract.d.ts.map +1 -1
  65. package/dist/render-graph-primitives.d.ts +2 -0
  66. package/dist/render-graph-primitives.d.ts.map +1 -1
  67. package/dist/render-pipeline.d.ts +13 -1
  68. package/dist/render-pipeline.d.ts.map +1 -1
  69. package/dist/render-system-extract-tail.d.ts.map +1 -1
  70. package/dist/render-system-extract.d.ts +18 -4
  71. package/dist/render-system-extract.d.ts.map +1 -1
  72. package/dist/render-system.d.ts +2 -1
  73. package/dist/render-system.d.ts.map +1 -1
  74. package/dist/temporal/index.mjs +5 -5
  75. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  76. package/package.json +21 -21
  77. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +12 -0
  78. package/src/__tests__/gpu-driven-production.integration.test.ts +27 -1
  79. package/src/__tests__/instances-store.unit.test.ts +15 -0
  80. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +28 -0
  81. package/src/assembly/webgpu-ready.ts +5 -5
  82. package/src/assembly/webgpu-renderer.ts +14 -14
  83. package/src/components/depth-of-field.ts +45 -0
  84. package/src/components/directional-light.ts +2 -1
  85. package/src/components/index.ts +1 -0
  86. package/src/extract/camera.ts +22 -2
  87. package/src/features/depth-of-field/__tests__/depth-of-field-params.unit.test.ts +943 -0
  88. package/src/features/depth-of-field/depth-of-field-assembly.ts +100 -0
  89. package/src/features/depth-of-field/depth-of-field-feature.ts +295 -0
  90. package/src/features/depth-of-field/depth-of-field-inspection.ts +237 -0
  91. package/src/features/depth-of-field/depth-of-field-params.ts +556 -0
  92. package/src/features/host.ts +1 -1
  93. package/src/features/types.ts +18 -1
  94. package/src/index.ts +32 -0
  95. package/src/inspection-types.ts +38 -0
  96. package/src/instances.ts +19 -5
  97. package/src/mesh-material-bindings.ts +4 -0
  98. package/src/pipeline/standard-output/graph.ts +3 -0
  99. package/src/pipeline/standard-output/types.ts +4 -1
  100. package/src/pipeline/standard-post.ts +21 -2
  101. package/src/plugin.ts +2 -0
  102. package/src/record/frame-snapshot.ts +26 -1
  103. package/src/record/frame.ts +39 -6
  104. package/src/record/main-pass.ts +1 -5
  105. package/src/record/typed-frame-graph.ts +106 -6
  106. package/src/recovery/render-system-candidate.ts +6 -0
  107. package/src/render-contract.ts +11 -0
  108. package/src/render-graph-primitives.ts +8 -1
  109. package/src/render-pipeline.ts +17 -1
  110. package/src/render-system-extract-tail.ts +87 -13
  111. package/src/render-system-extract.ts +103 -25
  112. package/src/render-system.ts +78 -0
  113. package/src/typed-render-graph-primitives.ts +3 -0
  114. package/dist/chunk-C3DDSXYK.mjs.map +0 -1
  115. package/dist/chunk-FUUO4UND.mjs.map +0 -1
  116. package/dist/chunk-JRBDO2KB.mjs.map +0 -1
  117. package/dist/chunk-PSEQ6QPH.mjs.map +0 -1
  118. package/dist/chunk-S5UE2T5P.mjs.map +0 -1
@@ -0,0 +1,100 @@
1
+ import type { ManifestEntry } from '@forgeax/engine-types';
2
+ import type {
3
+ PostProcessReadEntry,
4
+ PostProcessShaderEntry,
5
+ } from '../../fullscreen-post-process-pass';
6
+ import type { DepthOfFieldInspection } from '../../inspection-types';
7
+ import { DEPTH_OF_FIELD_POST_PROCESS_IDS } from './depth-of-field-feature';
8
+
9
+ export interface DepthOfFieldShaderSources {
10
+ readonly singleSample?: string;
11
+ readonly multisampled?: string;
12
+ }
13
+
14
+ export function dofInspection(inspection: DepthOfFieldInspection | undefined): {
15
+ readonly depthOfField?: DepthOfFieldInspection;
16
+ } {
17
+ return inspection === undefined ? {} : { depthOfField: Object.freeze(inspection) };
18
+ }
19
+
20
+ type DepthOfFieldRead = string | PostProcessReadEntry;
21
+ type DepthOfFieldPostProcessId = (typeof DEPTH_OF_FIELD_POST_PROCESS_IDS)[number];
22
+ type DepthOfFieldVariantSuffix = '' | '.msaa';
23
+ type DepthOfFieldRegistrar = {
24
+ readonly registerBuiltinPostProcess: (id: string, entry: PostProcessShaderEntry) => unknown;
25
+ };
26
+
27
+ const DEPTH_READ: PostProcessReadEntry = { key: 'scene-depth', sampleType: 'depth' };
28
+ const DEPTH_OF_FIELD_READS: Readonly<
29
+ Record<DepthOfFieldPostProcessId, readonly DepthOfFieldRead[]>
30
+ > = {
31
+ 'forgeax.dof.coc': ['scene-color', 'scene-temporal', DEPTH_READ],
32
+ 'forgeax.dof.prefilter': ['scene-color', 'dof-coc', DEPTH_READ],
33
+ 'forgeax.dof.prefilter.metadata': ['scene-color', 'scene-temporal', 'dof-coc', DEPTH_READ],
34
+ 'forgeax.dof.gather': [
35
+ 'dof-prefilter',
36
+ 'scene-temporal',
37
+ 'dof-coc',
38
+ 'dof-prefilter-metadata',
39
+ DEPTH_READ,
40
+ ],
41
+ 'forgeax.dof.background': ['scene-color', 'scene-temporal', 'dof-coc', DEPTH_READ],
42
+ 'forgeax.dof.composite': [
43
+ 'scene-color',
44
+ 'scene-temporal',
45
+ 'dof-coc',
46
+ 'dof-near',
47
+ 'dof-far',
48
+ 'dof-background',
49
+ DEPTH_READ,
50
+ ],
51
+ };
52
+
53
+ /** Register all DoF graph declarations against the active RenderSystem. */
54
+ export function registerDepthOfFieldBuiltins(
55
+ renderSystem: DepthOfFieldRegistrar,
56
+ entries: DepthOfFieldShaderSources,
57
+ ): void {
58
+ const register = (
59
+ baseId: DepthOfFieldPostProcessId,
60
+ source: string,
61
+ suffix: DepthOfFieldVariantSuffix,
62
+ ): void => {
63
+ const id = `${baseId}${suffix}`;
64
+ renderSystem.registerBuiltinPostProcess(id, {
65
+ source,
66
+ params: { byteSize: 64, defaultValue: new Uint8Array(64) },
67
+ reads: DEPTH_OF_FIELD_READS[baseId],
68
+ });
69
+ };
70
+ if (entries.singleSample !== undefined) {
71
+ for (const id of DEPTH_OF_FIELD_POST_PROCESS_IDS) register(id, entries.singleSample, '');
72
+ }
73
+ if (entries.multisampled !== undefined) {
74
+ for (const id of DEPTH_OF_FIELD_POST_PROCESS_IDS) register(id, entries.multisampled, '.msaa');
75
+ }
76
+ }
77
+
78
+ /** Prewarm both DoF shader variants before publishing renderer readiness. */
79
+ export async function prewarmDepthOfField(
80
+ manifestEntries: readonly ManifestEntry[],
81
+ prewarm: (entry: ManifestEntry | undefined, id: string) => Promise<void>,
82
+ register: (entries: DepthOfFieldShaderSources) => void,
83
+ ): Promise<void> {
84
+ let singleSample: ManifestEntry | undefined;
85
+ let multisampled: ManifestEntry | undefined;
86
+ for (const entry of manifestEntries) {
87
+ if (!entry.wgsl.includes('DepthOfFieldParams')) continue;
88
+ if (entry.wgsl.includes('texture_depth_multisampled_2d')) {
89
+ multisampled ??= entry;
90
+ } else {
91
+ singleSample ??= entry;
92
+ }
93
+ }
94
+ for (const id of DEPTH_OF_FIELD_POST_PROCESS_IDS) await prewarm(singleSample, id);
95
+ for (const id of DEPTH_OF_FIELD_POST_PROCESS_IDS) await prewarm(multisampled, `${id}.msaa`);
96
+ register({
97
+ ...(singleSample === undefined ? {} : { singleSample: singleSample.wgsl }),
98
+ ...(multisampled === undefined ? {} : { multisampled: multisampled.wgsl }),
99
+ });
100
+ }
@@ -0,0 +1,295 @@
1
+ import { type RenderGraphBuilder, RenderGraphError } from '@forgeax/engine-render-graph';
2
+ import { err, ok, type Result } from '@forgeax/engine-types';
3
+ import type { DepthOfFieldSide } from '../../components/depth-of-field';
4
+ import type { RenderExtent } from '../../pipeline/render-extent';
5
+ import { renderExtentSize } from '../../pipeline/render-extent';
6
+ import type { RenderPipelineFrame, RenderPipelineTarget } from '../../render-pipeline';
7
+ import { createRenderPipelineTarget } from '../../render-pipeline';
8
+ import { addTypedFullscreenPass } from '../../typed-render-graph-primitives';
9
+ import type { DepthOfFieldParams } from './depth-of-field-params';
10
+
11
+ export const DEPTH_OF_FIELD_FEATURE_IDENTITY = 'forgeax.depth-of-field';
12
+ export const DEPTH_OF_FIELD_COC_ID = 'forgeax.dof.coc';
13
+ export const DEPTH_OF_FIELD_PREFILTER_ID = 'forgeax.dof.prefilter';
14
+ export const DEPTH_OF_FIELD_PREFILTER_METADATA_ID = 'forgeax.dof.prefilter.metadata';
15
+ export const DEPTH_OF_FIELD_GATHER_ID = 'forgeax.dof.gather';
16
+ export const DEPTH_OF_FIELD_BACKGROUND_ID = 'forgeax.dof.background';
17
+ export const DEPTH_OF_FIELD_COMPOSITE_ID = 'forgeax.dof.composite';
18
+
19
+ /** All declarations are registered together so the build catalog is finite. */
20
+ export const DEPTH_OF_FIELD_POST_PROCESS_IDS = Object.freeze([
21
+ DEPTH_OF_FIELD_COC_ID,
22
+ DEPTH_OF_FIELD_PREFILTER_ID,
23
+ DEPTH_OF_FIELD_PREFILTER_METADATA_ID,
24
+ DEPTH_OF_FIELD_GATHER_ID,
25
+ DEPTH_OF_FIELD_BACKGROUND_ID,
26
+ DEPTH_OF_FIELD_COMPOSITE_ID,
27
+ ] as const);
28
+ export const DEPTH_OF_FIELD_MSAA_POST_PROCESS_IDS = Object.freeze(
29
+ DEPTH_OF_FIELD_POST_PROCESS_IDS.map((id) => `${id}.msaa` as const),
30
+ );
31
+
32
+ export function depthOfFieldPostProcessId(id: string, multisampled: boolean): string {
33
+ return multisampled ? `${id}.msaa` : id;
34
+ }
35
+
36
+ export interface DepthOfFieldTopology {
37
+ readonly blurSide: DepthOfFieldSide;
38
+ readonly useNear: boolean;
39
+ readonly useFar: boolean;
40
+ }
41
+
42
+ export function depthOfFieldTopology(
43
+ params: DepthOfFieldParams | undefined,
44
+ ): DepthOfFieldTopology | undefined {
45
+ if (params === undefined || params.maxRadiusPixels <= 0) return undefined;
46
+ return {
47
+ blurSide: params.blurSide,
48
+ useNear: params.blurSide === 'near' || params.blurSide === 'both',
49
+ useFar: params.blurSide === 'far' || params.blurSide === 'both',
50
+ };
51
+ }
52
+
53
+ function target(
54
+ graph: RenderGraphBuilder<RenderPipelineFrame>,
55
+ label: string,
56
+ descriptor: Parameters<RenderGraphBuilder<RenderPipelineFrame>['createTexture']>[1],
57
+ ): Result<RenderPipelineTarget, RenderGraphError> {
58
+ return createRenderPipelineTarget(graph, label, descriptor);
59
+ }
60
+
61
+ function addPass(
62
+ graph: RenderGraphBuilder<RenderPipelineFrame>,
63
+ options: Parameters<typeof addTypedFullscreenPass>[1],
64
+ ): Result<void, RenderGraphError> {
65
+ return addTypedFullscreenPass(graph, options);
66
+ }
67
+
68
+ function sideParams(params: DepthOfFieldParams, side: 'near' | 'far'): DepthOfFieldParams {
69
+ return Object.freeze({ ...params, blurSide: side });
70
+ }
71
+
72
+ /**
73
+ * Project the built-in spatial DoF graph after the shared temporal/metering
74
+ * seam. The graph owns every intermediate; the renderer only supplies UBO
75
+ * bytes and the manifest-backed fullscreen programs at record time.
76
+ */
77
+ export function addDepthOfFieldPasses(
78
+ context: {
79
+ readonly graph: RenderGraphBuilder<RenderPipelineFrame>;
80
+ readonly camera: {
81
+ readonly depthOfField?: DepthOfFieldParams | undefined;
82
+ };
83
+ readonly temporalTaa: boolean;
84
+ readonly msaaActive?: boolean;
85
+ /** Shared Standard pixel domains. DoF never invents a second extent. */
86
+ readonly extent?: RenderExtent;
87
+ },
88
+ input: RenderPipelineTarget,
89
+ depth: RenderPipelineTarget,
90
+ temporal?: RenderPipelineTarget,
91
+ ): Result<RenderPipelineTarget, RenderGraphError> {
92
+ const params = context.camera.depthOfField;
93
+ const topology = depthOfFieldTopology(params);
94
+ if (params === undefined || topology === undefined) return ok(input);
95
+
96
+ // The graph texture is the contract. Never select the multisampled shader
97
+ // from a camera flag while the bound depth target is single-sample.
98
+ const multisampled = depth.sampleCount === 4;
99
+ const graph = context.graph;
100
+ const extentSize =
101
+ context.extent === undefined
102
+ ? undefined
103
+ : renderExtentSize(context.extent, context.temporalTaa ? 'output' : 'internal');
104
+ const fullSize = extentSize ?? ('surface' as const);
105
+ const halfSize =
106
+ extentSize === undefined
107
+ ? ('half-surface' as const)
108
+ : {
109
+ width: Math.max(1, Math.ceil(extentSize.width / 2)),
110
+ height: Math.max(1, Math.ceil(extentSize.height / 2)),
111
+ };
112
+ const coc = target(graph, 'dof-coc', {
113
+ format: 'rgba16float',
114
+ size: fullSize,
115
+ domain: 'linear-hdr',
116
+ });
117
+ if (!coc.ok) return coc;
118
+ const cocPass = addPass(graph, {
119
+ name: 'dof-coc',
120
+ shader: depthOfFieldPostProcessId(DEPTH_OF_FIELD_COC_ID, multisampled),
121
+ fragmentEntryPoint: 'fs_coc',
122
+ input,
123
+ depth,
124
+ output: coc.value,
125
+ additionalReads: [
126
+ {
127
+ key: 'scene-temporal',
128
+ // No-TAA DoF deliberately aliases this optional input to current color;
129
+ // only the TAA route consumes the matching resolved temporal attachment.
130
+ target: context.temporalTaa && temporal !== undefined ? temporal : input,
131
+ },
132
+ ],
133
+ });
134
+ if (!cocPass.ok) return cocPass;
135
+
136
+ const near = topology.useNear
137
+ ? target(graph, 'dof-near', { format: 'rgba16float', size: halfSize })
138
+ : undefined;
139
+ if (near !== undefined && !near.ok) return near;
140
+ const far = topology.useFar
141
+ ? target(graph, 'dof-far', { format: 'rgba16float', size: halfSize })
142
+ : undefined;
143
+ if (far !== undefined && !far.ok) return far;
144
+ const background = topology.useNear
145
+ ? target(graph, 'dof-background', { format: 'rgba16float', size: halfSize })
146
+ : undefined;
147
+ if (background !== undefined && !background.ok) return background;
148
+
149
+ for (const side of ['near', 'far'] as const) {
150
+ const enabled = side === 'near' ? topology.useNear : topology.useFar;
151
+ if (!enabled) continue;
152
+ const prefilter = target(graph, `dof-prefilter-${side}`, {
153
+ format: 'rgba16float',
154
+ size: halfSize,
155
+ });
156
+ if (!prefilter.ok) return prefilter;
157
+ const blur = side === 'near' ? near : far;
158
+ if (blur === undefined || !blur.ok) {
159
+ return err(
160
+ new RenderGraphError({
161
+ code: 'resource-resolution-failed',
162
+ expected: `${side} DoF target is declared before its gather pass`,
163
+ hint: 'retain the last-known-good Standard graph and retry DoF admission',
164
+ detail: { resourceLabel: `dof-${side}` },
165
+ }),
166
+ );
167
+ }
168
+ const prefilterPass = addPass(graph, {
169
+ name: `dof-prefilter-${side}`,
170
+ shader: depthOfFieldPostProcessId(DEPTH_OF_FIELD_PREFILTER_ID, multisampled),
171
+ fragmentEntryPoint: side === 'near' ? 'fs_prefilter_near' : 'fs_prefilter_far',
172
+ input,
173
+ depth,
174
+ output: prefilter.value,
175
+ additionalReads: [{ key: 'dof-coc', target: coc.value }],
176
+ });
177
+ if (!prefilterPass.ok) return prefilterPass;
178
+ const prefilterMetadata = target(graph, `dof-prefilter-${side}-metadata`, {
179
+ format: 'rgba16float',
180
+ size: halfSize,
181
+ });
182
+ if (!prefilterMetadata.ok) return prefilterMetadata;
183
+ const metadataPass = addPass(graph, {
184
+ name: `dof-prefilter-${side}-metadata`,
185
+ shader: depthOfFieldPostProcessId(DEPTH_OF_FIELD_PREFILTER_METADATA_ID, multisampled),
186
+ fragmentEntryPoint:
187
+ side === 'near' ? 'fs_prefilter_metadata_near' : 'fs_prefilter_metadata_far',
188
+ input,
189
+ depth,
190
+ output: prefilterMetadata.value,
191
+ additionalReads: [
192
+ {
193
+ key: 'scene-temporal',
194
+ target: context.temporalTaa && temporal !== undefined ? temporal : input,
195
+ },
196
+ { key: 'dof-coc', target: coc.value },
197
+ ],
198
+ });
199
+ if (!metadataPass.ok) return metadataPass;
200
+ const gatherPass = addPass(graph, {
201
+ name: `dof-gather-${side}`,
202
+ shader: depthOfFieldPostProcessId(DEPTH_OF_FIELD_GATHER_ID, multisampled),
203
+ fragmentEntryPoint: side === 'near' ? 'fs_gather_near' : 'fs_gather_far',
204
+ input: prefilter.value,
205
+ depth,
206
+ output: blur.value,
207
+ additionalReads: [
208
+ {
209
+ key: 'scene-temporal',
210
+ target: context.temporalTaa && temporal !== undefined ? temporal : input,
211
+ },
212
+ { key: 'dof-coc', target: coc.value },
213
+ { key: 'dof-prefilter-metadata', target: prefilterMetadata.value },
214
+ ],
215
+ });
216
+ if (!gatherPass.ok) return gatherPass;
217
+ }
218
+
219
+ if (background?.ok) {
220
+ const backgroundPass = addPass(graph, {
221
+ name: 'dof-fill-background',
222
+ shader: depthOfFieldPostProcessId(DEPTH_OF_FIELD_BACKGROUND_ID, multisampled),
223
+ fragmentEntryPoint: 'fs_background',
224
+ input,
225
+ depth,
226
+ output: background.value,
227
+ additionalReads: [
228
+ {
229
+ key: 'scene-temporal',
230
+ target: context.temporalTaa && temporal !== undefined ? temporal : input,
231
+ },
232
+ { key: 'dof-coc', target: coc.value },
233
+ ],
234
+ });
235
+ if (!backgroundPass.ok) return backgroundPass;
236
+ }
237
+
238
+ const composite = target(graph, 'dof-composite', {
239
+ format: input.format,
240
+ size: fullSize,
241
+ domain: input.domain,
242
+ });
243
+ if (!composite.ok) return composite;
244
+ const nearInput = near?.ok === true ? near.value : input;
245
+ const farInput = far?.ok === true ? far.value : input;
246
+ const backgroundInput = background?.ok === true ? background.value : input;
247
+ const compositePass = addPass(graph, {
248
+ name: 'dof-composite',
249
+ shader: depthOfFieldPostProcessId(DEPTH_OF_FIELD_COMPOSITE_ID, multisampled),
250
+ fragmentEntryPoint: 'fs_composite',
251
+ input,
252
+ depth,
253
+ output: composite.value,
254
+ additionalReads: [
255
+ {
256
+ key: 'scene-temporal',
257
+ // The composite shader always owns the temporal-depth slot. Without
258
+ // TAA, the current input is the explicit alias for that slot.
259
+ target: context.temporalTaa && temporal !== undefined ? temporal : input,
260
+ },
261
+ { key: 'dof-coc', target: coc.value },
262
+ // Disabled sides alias the original input so their resources and gathers
263
+ // are absent while the fixed composite binding contract stays valid.
264
+ { key: 'dof-near', target: nearInput },
265
+ { key: 'dof-far', target: farInput },
266
+ { key: 'dof-background', target: backgroundInput },
267
+ ],
268
+ });
269
+ if (!compositePass.ok) return compositePass;
270
+ return ok(composite.value);
271
+ }
272
+
273
+ /** Return the bound pass names expected from an admitted DoF topology. */
274
+ export function depthOfFieldPassNames(topology: DepthOfFieldTopology): readonly string[] {
275
+ const names = ['dof-coc'];
276
+ if (topology.useNear)
277
+ names.push(
278
+ 'dof-prefilter-near',
279
+ 'dof-prefilter-near-metadata',
280
+ 'dof-gather-near',
281
+ 'dof-fill-background',
282
+ );
283
+ if (topology.useFar)
284
+ names.push('dof-prefilter-far', 'dof-prefilter-far-metadata', 'dof-gather-far');
285
+ names.push('dof-composite');
286
+ return Object.freeze(names);
287
+ }
288
+
289
+ /** Encode params with a side-local optical declaration for future debug views. */
290
+ export function depthOfFieldSideParams(
291
+ params: DepthOfFieldParams,
292
+ side: 'near' | 'far',
293
+ ): DepthOfFieldParams {
294
+ return sideParams(params, side);
295
+ }
@@ -0,0 +1,237 @@
1
+ import type { CompiledRenderGraphInfo } from '@forgeax/engine-render-graph';
2
+ import type { DepthOfFieldInspection } from '../../inspection-types';
3
+ import { depthOfFieldPassNames, depthOfFieldTopology } from './depth-of-field-feature';
4
+ import {
5
+ DEFAULT_DEPTH_OF_FIELD_PARAMS,
6
+ type DepthOfFieldParams,
7
+ type DepthOfFieldRequestFailure,
8
+ } from './depth-of-field-params';
9
+
10
+ const DOF_FORMAT_BYTES: Readonly<Record<string, number>> = Object.freeze({
11
+ rgba16float: 8,
12
+ rgba8unorm: 4,
13
+ });
14
+
15
+ function resourceBytes(resource: CompiledRenderGraphInfo['resources'][number]): number {
16
+ if (resource.byteSize !== undefined) return resource.byteSize;
17
+ const descriptor = resource.descriptor;
18
+ if (descriptor.kind !== 'texture') return 0;
19
+ const texelBytes = DOF_FORMAT_BYTES[descriptor.format] ?? 0;
20
+ if (texelBytes === 0) return 0;
21
+ let bytes = 0;
22
+ for (let level = 0; level < descriptor.mipLevelCount; level += 1) {
23
+ bytes +=
24
+ Math.max(1, descriptor.width >> level) *
25
+ Math.max(1, descriptor.height >> level) *
26
+ descriptor.depthOrArrayLayers *
27
+ texelBytes;
28
+ }
29
+ return bytes * descriptor.sampleCount;
30
+ }
31
+
32
+ function textureExtent(
33
+ graph: CompiledRenderGraphInfo,
34
+ label: string,
35
+ ): { readonly width: number; readonly height: number } | undefined {
36
+ const resource = graph.resources.find(
37
+ (candidate) => candidate.kind === 'texture' && candidate.label === label,
38
+ );
39
+ if (resource?.descriptor.kind !== 'texture') return undefined;
40
+ return { width: resource.descriptor.width, height: resource.descriptor.height };
41
+ }
42
+
43
+ export interface DepthOfFieldGraphInspection {
44
+ readonly status: 'empty' | 'active' | 'invalid';
45
+ readonly outputExtent: { readonly width: number; readonly height: number };
46
+ readonly workExtent: { readonly width: number; readonly height: number };
47
+ readonly passCount: number;
48
+ readonly textureBytes: number;
49
+ readonly graphGeneration: number;
50
+ }
51
+
52
+ export interface DepthOfFieldAcceptedState {
53
+ readonly params: DepthOfFieldParams;
54
+ readonly graph: CompiledRenderGraphInfo;
55
+ readonly deviceGeneration: number;
56
+ }
57
+
58
+ const emptyExtent = Object.freeze({ width: 0, height: 0 });
59
+ const OFF_PARAMS = Object.freeze({ ...DEFAULT_DEPTH_OF_FIELD_PARAMS, maxRadiusPixels: 0 });
60
+
61
+ /** Inspect only the compiled graph facts owned by the DoF feature. */
62
+ export function inspectDepthOfFieldGraph(
63
+ graph: CompiledRenderGraphInfo | undefined,
64
+ params: DepthOfFieldParams | undefined,
65
+ ): DepthOfFieldGraphInspection {
66
+ const topology = depthOfFieldTopology(params);
67
+ if (graph === undefined || topology === undefined) {
68
+ return {
69
+ status: 'empty',
70
+ outputExtent: emptyExtent,
71
+ workExtent: emptyExtent,
72
+ passCount: 0,
73
+ textureBytes: 0,
74
+ graphGeneration: graph?.generation ?? 0,
75
+ };
76
+ }
77
+ const expectedPasses = new Set(depthOfFieldPassNames(topology));
78
+ const passes = graph.passes.filter((pass) => expectedPasses.has(pass.name));
79
+ const dofResources = graph.resources.filter(
80
+ (resource) => resource.kind === 'texture' && resource.label.startsWith('dof-'),
81
+ );
82
+ const expectedResourceLabels = [
83
+ 'dof-coc',
84
+ ...(topology.useNear
85
+ ? ['dof-near', 'dof-prefilter-near', 'dof-prefilter-near-metadata', 'dof-background']
86
+ : []),
87
+ ...(topology.useFar ? ['dof-far', 'dof-prefilter-far', 'dof-prefilter-far-metadata'] : []),
88
+ 'dof-composite',
89
+ ];
90
+ const resourceLabels = new Set(dofResources.map((resource) => resource.label));
91
+ const completePasses =
92
+ passes.length === expectedPasses.size && passes.every((pass) => expectedPasses.has(pass.name));
93
+ const completeResources =
94
+ dofResources.length === expectedResourceLabels.length &&
95
+ expectedResourceLabels.every((label) => resourceLabels.has(label)) &&
96
+ dofResources.every((resource) => resourceBytes(resource) > 0);
97
+ const outputExtent = textureExtent(graph, 'dof-composite') ?? emptyExtent;
98
+ const workExtent = textureExtent(graph, topology.useNear ? 'dof-near' : 'dof-far') ?? emptyExtent;
99
+ return {
100
+ status: completePasses && completeResources ? 'active' : 'invalid',
101
+ outputExtent,
102
+ workExtent,
103
+ passCount: completePasses ? passes.length : 0,
104
+ textureBytes: completeResources
105
+ ? dofResources.reduce((bytes, resource) => bytes + resourceBytes(resource), 0)
106
+ : 0,
107
+ graphGeneration: graph.generation,
108
+ };
109
+ }
110
+
111
+ export function projectDepthOfFieldInspection(input: {
112
+ readonly params?: DepthOfFieldParams;
113
+ readonly graph?: CompiledRenderGraphInfo;
114
+ readonly deviceGeneration: number;
115
+ readonly error?: DepthOfFieldRequestFailure | undefined;
116
+ /** Explicit submit-bound state supplied by Renderer.inspect(). */
117
+ readonly accepted?: DepthOfFieldAcceptedState | undefined;
118
+ readonly lastSubmitFailed?: boolean | undefined;
119
+ }): DepthOfFieldInspection {
120
+ const params = input.params ?? OFF_PARAMS;
121
+ const topology = input.error === undefined ? depthOfFieldTopology(input.params) : undefined;
122
+ const requested = {
123
+ focusDistance: params.focusDistance,
124
+ fStop: params.fStop,
125
+ sensorHeight: params.sensorHeight,
126
+ maxRadiusPixels: params.maxRadiusPixels,
127
+ quality: params.quality,
128
+ blurSide: params.blurSide,
129
+ } as const;
130
+ const enabled = input.error !== undefined || topology !== undefined;
131
+ const transactionBound = 'accepted' in input;
132
+ const accepted = input.accepted;
133
+ const acceptedTopology = depthOfFieldTopology(accepted?.params);
134
+ const acceptedGraph =
135
+ accepted === undefined ? undefined : inspectDepthOfFieldGraph(accepted.graph, accepted.params);
136
+ const acceptedValid =
137
+ accepted !== undefined &&
138
+ accepted.deviceGeneration === input.deviceGeneration &&
139
+ acceptedTopology !== undefined &&
140
+ acceptedGraph?.status === 'active';
141
+ const candidateGraph = inspectDepthOfFieldGraph(input.graph, input.params);
142
+ // Renderer.inspect() is transaction-bound: a compiled candidate is useful
143
+ // for diagnosis, but it is not the effective graph until submit succeeds.
144
+ // Direct callers that omit `accepted` retain the original structural
145
+ // projection semantics used by graph-only tests and tools.
146
+ const graph = transactionBound
147
+ ? acceptedValid
148
+ ? (acceptedGraph ?? candidateGraph)
149
+ : candidateGraph
150
+ : candidateGraph;
151
+ const status: DepthOfFieldInspection['status'] =
152
+ input.error?.code === 'depth-of-field-orthographic-unsupported'
153
+ ? 'unsupported'
154
+ : input.error !== undefined
155
+ ? 'invalid'
156
+ : !enabled
157
+ ? 'off'
158
+ : transactionBound
159
+ ? acceptedValid && !input.lastSubmitFailed && requestedEqual(requested, accepted.params)
160
+ ? 'active'
161
+ : input.lastSubmitFailed
162
+ ? 'invalid'
163
+ : candidateGraph.status === 'invalid'
164
+ ? 'invalid'
165
+ : 'reset'
166
+ : graph.status === 'active'
167
+ ? 'active'
168
+ : graph.status === 'invalid'
169
+ ? 'invalid'
170
+ : 'reset';
171
+ const effective = acceptedValid
172
+ ? {
173
+ focusDistance: accepted.params.focusDistance,
174
+ fStop: accepted.params.fStop,
175
+ sensorHeight: accepted.params.sensorHeight,
176
+ maxRadiusPixels: accepted.params.maxRadiusPixels,
177
+ quality: accepted.params.quality,
178
+ blurSide: accepted.params.blurSide,
179
+ }
180
+ : status === 'active'
181
+ ? requested
182
+ : undefined;
183
+ const effectiveParams = acceptedValid
184
+ ? accepted.params
185
+ : status === 'active'
186
+ ? params
187
+ : undefined;
188
+ return Object.freeze({
189
+ enabled,
190
+ status,
191
+ requested,
192
+ ...(effective === undefined ? {} : { effective }),
193
+ focalLength: effectiveParams?.focalLength ?? params.focalLength,
194
+ outputExtent: graph.outputExtent,
195
+ workExtent: graph.workExtent,
196
+ tapCount:
197
+ effectiveParams !== undefined
198
+ ? effectiveParams.quality === 'low'
199
+ ? 16
200
+ : effectiveParams.quality === 'medium'
201
+ ? 32
202
+ : 64
203
+ : 0,
204
+ passCount: graph.passCount,
205
+ textureBytes: graph.textureBytes,
206
+ transparentPolicy: 'opaque-depth-approximation',
207
+ graphGeneration: graph.graphGeneration,
208
+ deviceGeneration: input.deviceGeneration,
209
+ lastKnownGood: acceptedValid || status === 'active',
210
+ ...(input.error === undefined ? {} : { error: input.error }),
211
+ ...(input.error?.code === 'depth-of-field-orthographic-unsupported'
212
+ ? { fallbackReason: 'orthographic-unsupported' as const }
213
+ : input.error !== undefined
214
+ ? { fallbackReason: 'invalid-params' as const }
215
+ : status === 'off'
216
+ ? { fallbackReason: 'zero-radius' as const }
217
+ : status === 'reset'
218
+ ? { fallbackReason: 'shader-not-ready' as const }
219
+ : status === 'invalid'
220
+ ? { fallbackReason: 'candidate-failed' as const }
221
+ : {}),
222
+ });
223
+ }
224
+
225
+ function requestedEqual(
226
+ requested: DepthOfFieldInspection['requested'],
227
+ params: DepthOfFieldParams,
228
+ ): boolean {
229
+ return (
230
+ requested.focusDistance === params.focusDistance &&
231
+ requested.fStop === params.fStop &&
232
+ requested.sensorHeight === params.sensorHeight &&
233
+ requested.maxRadiusPixels === params.maxRadiusPixels &&
234
+ requested.quality === params.quality &&
235
+ requested.blurSide === params.blurSide
236
+ );
237
+ }