@forgeax/engine-render 0.1.31 → 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.
- package/README.md +68 -1
- package/dist/assembly/webgpu-ready.d.ts +2 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/{chunk-C3DDSXYK.mjs → chunk-4NNZQCL3.mjs} +110 -35
- package/dist/chunk-4NNZQCL3.mjs.map +1 -0
- package/dist/{chunk-JRBDO2KB.mjs → chunk-6TFBXLRJ.mjs} +19 -7
- package/dist/chunk-6TFBXLRJ.mjs.map +1 -0
- package/dist/{chunk-FUUO4UND.mjs → chunk-DRR7D3KX.mjs} +3 -3
- package/dist/chunk-DRR7D3KX.mjs.map +1 -0
- package/dist/{chunk-S5UE2T5P.mjs → chunk-GWCEMDVR.mjs} +380 -4
- package/dist/chunk-GWCEMDVR.mjs.map +1 -0
- package/dist/{chunk-MDQJPY3T.mjs → chunk-JCIRYQ4V.mjs} +3 -3
- package/dist/{chunk-MDQJPY3T.mjs.map → chunk-JCIRYQ4V.mjs.map} +1 -1
- package/dist/{chunk-PSEQ6QPH.mjs → chunk-JRIPCMYC.mjs} +17 -10
- package/dist/chunk-JRIPCMYC.mjs.map +1 -0
- package/dist/{chunk-7MQ3XBSP.mjs → chunk-LULCA3OF.mjs} +4 -4
- package/dist/{chunk-7MQ3XBSP.mjs.map → chunk-LULCA3OF.mjs.map} +1 -1
- package/dist/{chunk-NO4WZCAH.mjs → chunk-S665NJNF.mjs} +3 -3
- package/dist/{chunk-NO4WZCAH.mjs.map → chunk-S665NJNF.mjs.map} +1 -1
- package/dist/components/depth-of-field.d.ts +28 -0
- package/dist/components/depth-of-field.d.ts.map +1 -0
- package/dist/components/directional-light.d.ts +2 -1
- package/dist/components/directional-light.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +898 -328
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/features/depth-of-field/depth-of-field-assembly.d.ts +19 -0
- package/dist/features/depth-of-field/depth-of-field-assembly.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-feature.d.ts +43 -0
- package/dist/features/depth-of-field/depth-of-field-feature.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-inspection.d.ts +34 -0
- package/dist/features/depth-of-field/depth-of-field-inspection.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-params.d.ts +83 -0
- package/dist/features/depth-of-field/depth-of-field-params.d.ts.map +1 -0
- package/dist/features/types.d.ts +19 -1
- package/dist/features/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6 -5
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +35 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances.d.ts +7 -0
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +6 -6
- package/dist/mesh-material-bindings.d.ts +3 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
- package/dist/pipeline/standard-output/types.d.ts +4 -2
- package/dist/pipeline/standard-output/types.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +21 -1
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +7 -0
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +8 -1
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts +2 -0
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +13 -1
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +18 -4
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +2 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/gpu-driven-baseline.characterization.test.ts +12 -0
- package/src/__tests__/gpu-driven-production.integration.test.ts +27 -1
- package/src/__tests__/instances-store.unit.test.ts +15 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +28 -0
- package/src/assembly/webgpu-ready.ts +5 -5
- package/src/assembly/webgpu-renderer.ts +14 -14
- package/src/components/depth-of-field.ts +45 -0
- package/src/components/directional-light.ts +2 -1
- package/src/components/index.ts +1 -0
- package/src/extract/camera.ts +22 -2
- package/src/features/depth-of-field/__tests__/depth-of-field-params.unit.test.ts +943 -0
- package/src/features/depth-of-field/depth-of-field-assembly.ts +100 -0
- package/src/features/depth-of-field/depth-of-field-feature.ts +295 -0
- package/src/features/depth-of-field/depth-of-field-inspection.ts +237 -0
- package/src/features/depth-of-field/depth-of-field-params.ts +556 -0
- package/src/features/host.ts +1 -1
- package/src/features/types.ts +18 -1
- package/src/index.ts +32 -0
- package/src/inspection-types.ts +38 -0
- package/src/instances.ts +19 -5
- package/src/mesh-material-bindings.ts +4 -0
- package/src/pipeline/standard-output/graph.ts +3 -0
- package/src/pipeline/standard-output/types.ts +4 -1
- package/src/pipeline/standard-post.ts +21 -2
- package/src/plugin.ts +2 -0
- package/src/record/frame-snapshot.ts +26 -1
- package/src/record/frame.ts +39 -6
- package/src/record/main-pass.ts +1 -5
- package/src/record/typed-frame-graph.ts +106 -6
- package/src/recovery/render-system-candidate.ts +6 -0
- package/src/render-contract.ts +11 -0
- package/src/render-graph-primitives.ts +8 -1
- package/src/render-pipeline.ts +17 -1
- package/src/render-system-extract-tail.ts +87 -13
- package/src/render-system-extract.ts +103 -25
- package/src/render-system.ts +78 -0
- package/src/typed-render-graph-primitives.ts +3 -0
- package/dist/chunk-C3DDSXYK.mjs.map +0 -1
- package/dist/chunk-FUUO4UND.mjs.map +0 -1
- package/dist/chunk-JRBDO2KB.mjs.map +0 -1
- package/dist/chunk-PSEQ6QPH.mjs.map +0 -1
- 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
|
+
}
|