@forgeax/engine-render-graph 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +184 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/observation.unit.test.d.ts +2 -0
- package/dist/__tests__/observation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph-alias-source.unit.test.d.ts +2 -0
- package/dist/__tests__/render-graph-alias-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph-builder.unit.test.d.ts +2 -0
- package/dist/__tests__/render-graph-builder.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph-errors.test-d.d.ts +2 -0
- package/dist/__tests__/render-graph-errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-graph-regressions.unit.test.d.ts +2 -0
- package/dist/__tests__/render-graph-regressions.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph-rhi-null.integration.test.d.ts +2 -0
- package/dist/__tests__/render-graph-rhi-null.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-graph.unit.test.d.ts +2 -0
- package/dist/__tests__/render-graph.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resource-declaration-collision.test.d.ts +2 -0
- package/dist/__tests__/resource-declaration-collision.test.d.ts.map +1 -0
- package/dist/builder.d.ts +42 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/compiled-graph.d.ts +23 -0
- package/dist/compiled-graph.d.ts.map +1 -0
- package/dist/errors.d.ts +148 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graph.d.ts +403 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +2256 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kernel-internal.d.ts +84 -0
- package/dist/kernel-internal.d.ts.map +1 -0
- package/dist/observation.d.ts +38 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/pass-registry.d.ts +12 -0
- package/dist/pass-registry.d.ts.map +1 -0
- package/dist/pipeline/__tests__/color-value-domain.test.d.ts +2 -0
- package/dist/pipeline/__tests__/color-value-domain.test.d.ts.map +1 -0
- package/dist/pipeline/color-value-domain.d.ts +35 -0
- package/dist/pipeline/color-value-domain.d.ts.map +1 -0
- package/dist/resource-registry.d.ts +62 -0
- package/dist/resource-registry.d.ts.map +1 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/__tests__/observation.unit.test.ts +103 -0
- package/src/__tests__/render-graph-alias-source.unit.test.ts +118 -0
- package/src/__tests__/render-graph-builder.unit.test.ts +673 -0
- package/src/__tests__/render-graph-errors.test-d.ts +207 -0
- package/src/__tests__/render-graph-regressions.unit.test.ts +126 -0
- package/src/__tests__/render-graph-rhi-null.integration.test.ts +86 -0
- package/src/__tests__/render-graph.unit.test.ts +2551 -0
- package/src/__tests__/resource-declaration-collision.test.ts +151 -0
- package/src/builder.ts +1009 -0
- package/src/compiled-graph.ts +383 -0
- package/src/errors.ts +205 -0
- package/src/graph.ts +1269 -0
- package/src/index.ts +97 -0
- package/src/kernel-internal.ts +148 -0
- package/src/observation.ts +134 -0
- package/src/pass-registry.ts +43 -0
- package/src/pipeline/__tests__/color-value-domain.test.ts +43 -0
- package/src/pipeline/color-value-domain.ts +139 -0
- package/src/resource-registry.ts +174 -0
- package/src/types.ts +216 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// @forgeax/engine-render-graph -- declarative render-graph abstraction.
|
|
2
|
+
//
|
|
3
|
+
// Public surface (charter proposition 1: progressive disclosure):
|
|
4
|
+
// - RenderGraph / PassDescriptor / ResourceDescriptor -- core primitives
|
|
5
|
+
// - RenderGraphError / RenderGraphErrorCode -- closed-union error model
|
|
6
|
+
// - Result<T, E> / ok / err -- binary result type
|
|
7
|
+
// - PassInfo / ResourceInfo -- query interfaces
|
|
8
|
+
|
|
9
|
+
export { RenderGraphBuilder } from './builder.js';
|
|
10
|
+
export {
|
|
11
|
+
err,
|
|
12
|
+
type InvalidFormatDetail,
|
|
13
|
+
ok,
|
|
14
|
+
RenderGraphError,
|
|
15
|
+
type RenderGraphErrorCode,
|
|
16
|
+
type RenderGraphErrorDetail,
|
|
17
|
+
type ResourceAllocFailedDetail,
|
|
18
|
+
type Result,
|
|
19
|
+
type ResultErr,
|
|
20
|
+
type ResultOk,
|
|
21
|
+
} from './errors.js';
|
|
22
|
+
export type {
|
|
23
|
+
BufferRole,
|
|
24
|
+
ColorTargetDescriptor,
|
|
25
|
+
ColorTargetHandle,
|
|
26
|
+
ColorTargetSize,
|
|
27
|
+
CompileOptions,
|
|
28
|
+
ComputePassDescriptor,
|
|
29
|
+
InternalizedGraph,
|
|
30
|
+
InternalizedPass,
|
|
31
|
+
PassDescriptor,
|
|
32
|
+
PassInfo,
|
|
33
|
+
ResolveContext,
|
|
34
|
+
ResolvedBuffer,
|
|
35
|
+
ResolvedBufferType,
|
|
36
|
+
ResolvedColorTargetDescriptor,
|
|
37
|
+
ResourceDescriptor,
|
|
38
|
+
ResourceInfo,
|
|
39
|
+
ResourceKind,
|
|
40
|
+
ResourceLifetime,
|
|
41
|
+
} from './graph.js';
|
|
42
|
+
export { RenderGraph } from './graph.js';
|
|
43
|
+
export {
|
|
44
|
+
type CurrentFrameObservationDescriptor,
|
|
45
|
+
type CurrentFrameObservationLease,
|
|
46
|
+
type CurrentFrameObservationLifetime,
|
|
47
|
+
type CurrentFrameObservationSource,
|
|
48
|
+
createCurrentFrameObservationLease,
|
|
49
|
+
type ObservationSize,
|
|
50
|
+
} from './observation.js';
|
|
51
|
+
export {
|
|
52
|
+
COLOR_VALUE_DOMAINS,
|
|
53
|
+
type ColorDomainConnection,
|
|
54
|
+
type ColorDomainConversion,
|
|
55
|
+
type ColorDomainValidation,
|
|
56
|
+
type ColorResourceDescriptor,
|
|
57
|
+
type ColorValueDomain,
|
|
58
|
+
deserializeColorResourceDescriptor,
|
|
59
|
+
deserializeColorValueDomain,
|
|
60
|
+
isColorValueDomain,
|
|
61
|
+
serializeColorResourceDescriptor,
|
|
62
|
+
serializeColorValueDomain,
|
|
63
|
+
validateColorDomainConnection,
|
|
64
|
+
} from './pipeline/color-value-domain.js';
|
|
65
|
+
export type {
|
|
66
|
+
CompiledRenderGraph,
|
|
67
|
+
CompiledRenderGraphInfo,
|
|
68
|
+
ComputeGraphPass,
|
|
69
|
+
CopyGraphPass,
|
|
70
|
+
GraphAccess,
|
|
71
|
+
GraphAccessInfo,
|
|
72
|
+
GraphBuffer,
|
|
73
|
+
GraphBufferAccess,
|
|
74
|
+
GraphBufferDescriptor,
|
|
75
|
+
GraphExtent,
|
|
76
|
+
GraphPass,
|
|
77
|
+
GraphPassKind,
|
|
78
|
+
GraphResource,
|
|
79
|
+
GraphResourceKind,
|
|
80
|
+
GraphResourceOrigin,
|
|
81
|
+
GraphResourceResolver,
|
|
82
|
+
GraphTexture,
|
|
83
|
+
GraphTextureAccess,
|
|
84
|
+
GraphTextureDescriptor,
|
|
85
|
+
GraphTextureView,
|
|
86
|
+
GraphTextureViewDescriptor,
|
|
87
|
+
ImportedBufferDescriptor,
|
|
88
|
+
ImportedTextureDescriptor,
|
|
89
|
+
ImportedTextureViewResolver,
|
|
90
|
+
RasterColorAttachment,
|
|
91
|
+
RasterDepthStencilAttachment,
|
|
92
|
+
RasterGraphPass,
|
|
93
|
+
RenderGraphCompileOptions,
|
|
94
|
+
RenderGraphFrame,
|
|
95
|
+
RenderGraphPassExecution,
|
|
96
|
+
RenderGraphPassRunner,
|
|
97
|
+
} from './types.js';
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Buffer, Texture, TextureView } from '@forgeax/engine-rhi';
|
|
2
|
+
import type {
|
|
3
|
+
GraphAccess,
|
|
4
|
+
GraphBuffer,
|
|
5
|
+
GraphBufferDescriptor,
|
|
6
|
+
GraphPass,
|
|
7
|
+
GraphResourceKind,
|
|
8
|
+
GraphResourceOrigin,
|
|
9
|
+
GraphTexture,
|
|
10
|
+
GraphTextureDescriptor,
|
|
11
|
+
GraphTextureView,
|
|
12
|
+
GraphTextureViewDescriptor,
|
|
13
|
+
ImportedBufferDescriptor,
|
|
14
|
+
ImportedTextureDescriptor,
|
|
15
|
+
ImportedTextureViewResolver,
|
|
16
|
+
RenderGraphFrame,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
export interface TextureHandleData {
|
|
20
|
+
readonly owner: object;
|
|
21
|
+
readonly id: number;
|
|
22
|
+
readonly kind: 'texture';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TextureViewHandleData {
|
|
26
|
+
readonly owner: object;
|
|
27
|
+
readonly id: number;
|
|
28
|
+
readonly kind: 'texture-view';
|
|
29
|
+
readonly textureId: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BufferHandleData {
|
|
33
|
+
readonly owner: object;
|
|
34
|
+
readonly id: number;
|
|
35
|
+
readonly kind: 'buffer';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ResourceHandleData = TextureHandleData | TextureViewHandleData | BufferHandleData;
|
|
39
|
+
|
|
40
|
+
export function textureHandle(owner: object, id: number): GraphTexture {
|
|
41
|
+
return Object.freeze({ owner, id, kind: 'texture' }) as unknown as GraphTexture;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function textureViewHandle(owner: object, id: number, textureId: number): GraphTextureView {
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
owner,
|
|
47
|
+
id,
|
|
48
|
+
kind: 'texture-view',
|
|
49
|
+
textureId,
|
|
50
|
+
}) as unknown as GraphTextureView;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function bufferHandle(owner: object, id: number): GraphBuffer {
|
|
54
|
+
return Object.freeze({ owner, id, kind: 'buffer' }) as unknown as GraphBuffer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function handleData(resource: unknown): ResourceHandleData | undefined {
|
|
58
|
+
if (typeof resource !== 'object' || resource === null) return undefined;
|
|
59
|
+
const candidate = resource as Partial<ResourceHandleData>;
|
|
60
|
+
if (typeof candidate.id !== 'number' || typeof candidate.owner !== 'object') return undefined;
|
|
61
|
+
if (
|
|
62
|
+
candidate.kind !== 'texture' &&
|
|
63
|
+
candidate.kind !== 'texture-view' &&
|
|
64
|
+
candidate.kind !== 'buffer'
|
|
65
|
+
) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return candidate as ResourceHandleData;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ResourceRecordBase {
|
|
72
|
+
readonly id: number;
|
|
73
|
+
readonly label: string;
|
|
74
|
+
readonly kind: GraphResourceKind;
|
|
75
|
+
readonly origin: GraphResourceOrigin;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CreatedTextureRecord extends ResourceRecordBase {
|
|
79
|
+
readonly kind: 'texture';
|
|
80
|
+
readonly origin: 'created';
|
|
81
|
+
readonly descriptor: GraphTextureDescriptor;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ImportedTextureRecord<FrameCtx> extends ResourceRecordBase {
|
|
85
|
+
readonly kind: 'texture';
|
|
86
|
+
readonly origin: 'imported';
|
|
87
|
+
readonly descriptor: ImportedTextureDescriptor;
|
|
88
|
+
readonly resolve: (frame: FrameCtx) => Texture;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CreatedBufferRecord extends ResourceRecordBase {
|
|
92
|
+
readonly kind: 'buffer';
|
|
93
|
+
readonly origin: 'created';
|
|
94
|
+
readonly descriptor: GraphBufferDescriptor;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ImportedBufferRecord<FrameCtx> extends ResourceRecordBase {
|
|
98
|
+
readonly kind: 'buffer';
|
|
99
|
+
readonly origin: 'imported';
|
|
100
|
+
readonly descriptor: ImportedBufferDescriptor;
|
|
101
|
+
readonly resolve: (frame: FrameCtx) => Buffer;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type ResourceRecord<FrameCtx> =
|
|
105
|
+
| CreatedTextureRecord
|
|
106
|
+
| ImportedTextureRecord<FrameCtx>
|
|
107
|
+
| CreatedBufferRecord
|
|
108
|
+
| ImportedBufferRecord<FrameCtx>;
|
|
109
|
+
|
|
110
|
+
export interface TextureViewRecord<FrameCtx extends RenderGraphFrame = RenderGraphFrame> {
|
|
111
|
+
readonly id: number;
|
|
112
|
+
readonly label: string;
|
|
113
|
+
readonly textureId: number;
|
|
114
|
+
readonly descriptor: GraphTextureViewDescriptor;
|
|
115
|
+
readonly resolve?: ImportedTextureViewResolver<FrameCtx> | undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface PassRecord<FrameCtx extends RenderGraphFrame> {
|
|
119
|
+
readonly id: number;
|
|
120
|
+
readonly name: string;
|
|
121
|
+
readonly pass: GraphPass<FrameCtx>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface CompiledResource<FrameCtx> {
|
|
125
|
+
readonly record: ResourceRecord<FrameCtx>;
|
|
126
|
+
readonly usage: number;
|
|
127
|
+
readonly firstUse: number | null;
|
|
128
|
+
readonly lastUse: number | null;
|
|
129
|
+
readonly texture?: Texture | undefined;
|
|
130
|
+
readonly buffer?: Buffer | undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface CompiledView<FrameCtx extends RenderGraphFrame = RenderGraphFrame> {
|
|
134
|
+
readonly record: TextureViewRecord<FrameCtx>;
|
|
135
|
+
readonly view?: TextureView | undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface CompiledPass<FrameCtx extends RenderGraphFrame> extends PassRecord<FrameCtx> {
|
|
139
|
+
readonly dependencies: readonly number[];
|
|
140
|
+
readonly resourceIds: ReadonlySet<number>;
|
|
141
|
+
readonly viewIds: ReadonlySet<number>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function accessResourceId(access: GraphAccess): number | undefined {
|
|
145
|
+
const data = handleData(access.resource);
|
|
146
|
+
if (data?.kind === 'texture-view') return data.textureId;
|
|
147
|
+
return data?.id;
|
|
148
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Texture, TextureFormat } from '@forgeax/engine-rhi';
|
|
2
|
+
import { err, ok, RenderGraphError, type Result } from './errors.js';
|
|
3
|
+
|
|
4
|
+
export { RenderGraphError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
const COPY_SRC = 0x01;
|
|
7
|
+
|
|
8
|
+
export interface ObservationSize {
|
|
9
|
+
readonly width: number;
|
|
10
|
+
readonly height: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Generic producer-owned metadata for one current-frame texture. */
|
|
14
|
+
export interface CurrentFrameObservationDescriptor {
|
|
15
|
+
readonly texture: Texture;
|
|
16
|
+
readonly format: TextureFormat;
|
|
17
|
+
readonly size: ObservationSize;
|
|
18
|
+
readonly usage: number;
|
|
19
|
+
readonly frameId: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CurrentFrameObservationLifetime {
|
|
23
|
+
readonly frameId: number;
|
|
24
|
+
readonly state: 'active' | 'retired';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The only resource information a downstream copy consumer receives. */
|
|
28
|
+
export interface CurrentFrameObservationSource {
|
|
29
|
+
readonly texture: Texture;
|
|
30
|
+
readonly descriptor: CurrentFrameObservationDescriptor;
|
|
31
|
+
readonly lifetime: CurrentFrameObservationLifetime;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CurrentFrameObservationLease {
|
|
35
|
+
readonly descriptor: CurrentFrameObservationDescriptor;
|
|
36
|
+
readonly lifetime: CurrentFrameObservationLifetime;
|
|
37
|
+
readonly state: 'active' | 'retired';
|
|
38
|
+
beginReadback(): Result<CurrentFrameObservationSource, RenderGraphError>;
|
|
39
|
+
retire(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function observationError(
|
|
43
|
+
code:
|
|
44
|
+
| 'observation-absent'
|
|
45
|
+
| 'observation-invalid-format'
|
|
46
|
+
| 'observation-invalid-size'
|
|
47
|
+
| 'observation-missing-copy-src'
|
|
48
|
+
| 'observation-stale',
|
|
49
|
+
expected: string,
|
|
50
|
+
hint: string,
|
|
51
|
+
): Result<never, RenderGraphError> {
|
|
52
|
+
return err(new RenderGraphError({ code, expected, hint }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate and lease a producer's current-frame texture without naming any
|
|
57
|
+
* semantic, pipeline, backend policy, or graph resource key.
|
|
58
|
+
*/
|
|
59
|
+
export function createCurrentFrameObservationLease(
|
|
60
|
+
descriptor: CurrentFrameObservationDescriptor,
|
|
61
|
+
currentFrameId: number,
|
|
62
|
+
): Result<CurrentFrameObservationLease, RenderGraphError> {
|
|
63
|
+
if (descriptor.texture === undefined || descriptor.texture === null) {
|
|
64
|
+
return observationError(
|
|
65
|
+
'observation-absent',
|
|
66
|
+
'a producer-owned texture handle',
|
|
67
|
+
'provide the current frame color texture before requesting an observation',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (descriptor.format !== 'rgba16float') {
|
|
71
|
+
return observationError(
|
|
72
|
+
'observation-invalid-format',
|
|
73
|
+
"current-frame observation format 'rgba16float'",
|
|
74
|
+
'use the producer target format without reinterpretation',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
!Number.isInteger(descriptor.size.width) ||
|
|
79
|
+
!Number.isInteger(descriptor.size.height) ||
|
|
80
|
+
descriptor.size.width <= 0 ||
|
|
81
|
+
descriptor.size.height <= 0
|
|
82
|
+
) {
|
|
83
|
+
return observationError(
|
|
84
|
+
'observation-invalid-size',
|
|
85
|
+
'positive integer observation width and height',
|
|
86
|
+
'capture a non-empty current-frame target',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if ((descriptor.usage & COPY_SRC) === 0) {
|
|
90
|
+
return observationError(
|
|
91
|
+
'observation-missing-copy-src',
|
|
92
|
+
'current-frame texture usage includes COPY_SRC',
|
|
93
|
+
'add COPY_SRC to the producer target before requesting readback',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (descriptor.frameId !== currentFrameId) {
|
|
97
|
+
return observationError(
|
|
98
|
+
'observation-stale',
|
|
99
|
+
`observation frame ${currentFrameId}`,
|
|
100
|
+
`discard frame ${descriptor.frameId} and request the current producer target`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let state: 'active' | 'retired' = 'active';
|
|
105
|
+
const lifetime: CurrentFrameObservationLifetime = {
|
|
106
|
+
frameId: descriptor.frameId,
|
|
107
|
+
get state() {
|
|
108
|
+
return state;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const lease: CurrentFrameObservationLease = {
|
|
112
|
+
descriptor,
|
|
113
|
+
lifetime,
|
|
114
|
+
get state() {
|
|
115
|
+
return state;
|
|
116
|
+
},
|
|
117
|
+
beginReadback() {
|
|
118
|
+
if (state === 'retired') {
|
|
119
|
+
return err(
|
|
120
|
+
new RenderGraphError({
|
|
121
|
+
code: 'observation-retired',
|
|
122
|
+
expected: 'active current-frame observation lease',
|
|
123
|
+
hint: 'submit the eager copy before the producer retires this frame',
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return ok({ texture: descriptor.texture, descriptor, lifetime });
|
|
128
|
+
},
|
|
129
|
+
retire() {
|
|
130
|
+
state = 'retired';
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
return ok(lease);
|
|
134
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @forgeax/engine-render-graph/src/pass-registry.ts — pass declaration
|
|
2
|
+
// registry (plan-strategy 3.1).
|
|
3
|
+
//
|
|
4
|
+
// Shape (D-5):
|
|
5
|
+
// - name -> reads/writes declaration + optional execute closure
|
|
6
|
+
// - supports listPasses enumeration
|
|
7
|
+
|
|
8
|
+
import { RenderGraphError } from './errors.js';
|
|
9
|
+
import type { PassDescriptor } from './graph.js';
|
|
10
|
+
|
|
11
|
+
export interface PassEntry<Ctx = unknown> {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly descriptor: PassDescriptor<Ctx>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class PassRegistry<Ctx = unknown> {
|
|
17
|
+
private readonly passes: PassEntry<Ctx>[] = [];
|
|
18
|
+
|
|
19
|
+
add(name: string, descriptor: PassDescriptor<Ctx>, before?: string): PassEntry<Ctx> {
|
|
20
|
+
if (this.passes.some((pass) => pass.name === name)) {
|
|
21
|
+
throw new RenderGraphError({
|
|
22
|
+
code: 'duplicate-pass-name',
|
|
23
|
+
expected: `pass name '${name}' is unique within one graph`,
|
|
24
|
+
hint: `rename the second '${name}' pass; labels are diagnostics, not identity`,
|
|
25
|
+
detail: { passName: name },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const entry: PassEntry<Ctx> = { name, descriptor };
|
|
29
|
+
const beforeIndex =
|
|
30
|
+
before === undefined ? -1 : this.passes.findIndex((pass) => pass.name === before);
|
|
31
|
+
if (beforeIndex < 0) this.passes.push(entry);
|
|
32
|
+
else this.passes.splice(beforeIndex, 0, entry);
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
list(): readonly PassEntry<Ctx>[] {
|
|
37
|
+
return this.passes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
count(): number {
|
|
41
|
+
return this.passes.length;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
COLOR_VALUE_DOMAINS,
|
|
4
|
+
type ColorResourceDescriptor,
|
|
5
|
+
type ColorValueDomain,
|
|
6
|
+
deserializeColorValueDomain,
|
|
7
|
+
serializeColorResourceDescriptor,
|
|
8
|
+
serializeColorValueDomain,
|
|
9
|
+
} from '../color-value-domain';
|
|
10
|
+
|
|
11
|
+
describe('ColorValueDomain', () => {
|
|
12
|
+
it('keeps the closed domain union in pipeline order', () => {
|
|
13
|
+
expect(COLOR_VALUE_DOMAINS).toEqual(['linear-hdr', 'linear-ldr', 'display-encoded']);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
'linear-hdr',
|
|
18
|
+
'linear-ldr',
|
|
19
|
+
'display-encoded',
|
|
20
|
+
] as const)('round-trips the %s domain through serialization', (domain: ColorValueDomain) => {
|
|
21
|
+
expect(deserializeColorValueDomain(serializeColorValueDomain(domain))).toEqual({
|
|
22
|
+
ok: true,
|
|
23
|
+
value: domain,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('serializes a resource descriptor without using the attachment format as authority', () => {
|
|
28
|
+
const descriptor: ColorResourceDescriptor = {
|
|
29
|
+
domain: 'linear-hdr',
|
|
30
|
+
format: 'rgba8unorm',
|
|
31
|
+
};
|
|
32
|
+
expect(JSON.parse(serializeColorResourceDescriptor(descriptor))).toEqual(descriptor);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each([undefined, 'gamma-magic'])('rejects a missing or unknown domain: %s', (value) => {
|
|
36
|
+
const result = deserializeColorValueDomain(value);
|
|
37
|
+
expect(result.ok).toBe(false);
|
|
38
|
+
if (result.ok) throw new Error('expected an invalid color domain');
|
|
39
|
+
expect(result.error.code).toBe('invalid-color-domain');
|
|
40
|
+
expect(result.error.expected).toContain('linear-hdr');
|
|
41
|
+
expect(result.error.hint).toContain('domain');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { err, ok, RenderGraphError, type Result } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
export const COLOR_VALUE_DOMAINS = ['linear-hdr', 'linear-ldr', 'display-encoded'] as const;
|
|
4
|
+
|
|
5
|
+
export type ColorValueDomain = (typeof COLOR_VALUE_DOMAINS)[number];
|
|
6
|
+
|
|
7
|
+
export type ColorDomainConversion =
|
|
8
|
+
| { readonly kind: 'encode-srgb' }
|
|
9
|
+
| { readonly kind: 'decode-srgb' }
|
|
10
|
+
| { readonly kind: 'tone-map' };
|
|
11
|
+
|
|
12
|
+
export interface ColorResourceDescriptor {
|
|
13
|
+
readonly domain: ColorValueDomain;
|
|
14
|
+
readonly format: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ColorDomainConnection {
|
|
18
|
+
readonly source: string;
|
|
19
|
+
readonly destination: string;
|
|
20
|
+
readonly conversion?: ColorDomainConversion | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ColorDomainValidation =
|
|
24
|
+
| { readonly ok: true }
|
|
25
|
+
| { readonly ok: false; readonly error: RenderGraphError };
|
|
26
|
+
|
|
27
|
+
export function isColorValueDomain(value: unknown): value is ColorValueDomain {
|
|
28
|
+
return typeof value === 'string' && COLOR_VALUE_DOMAINS.includes(value as ColorValueDomain);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function invalidDomain(value: unknown): RenderGraphError {
|
|
32
|
+
return new RenderGraphError({
|
|
33
|
+
code: 'invalid-color-domain',
|
|
34
|
+
expected: `domain is one of ${COLOR_VALUE_DOMAINS.join(', ')}`,
|
|
35
|
+
hint: 'set an explicit color domain; do not infer it from the attachment format',
|
|
36
|
+
detail: { value: String(value) },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function missingDomain(resourceKey?: string): RenderGraphError {
|
|
41
|
+
return new RenderGraphError({
|
|
42
|
+
code: 'missing-color-domain',
|
|
43
|
+
expected: 'every connected color resource has an explicit domain',
|
|
44
|
+
hint: `add domain to the color resource descriptor${resourceKey === undefined ? '' : ` '${resourceKey}'`}`,
|
|
45
|
+
detail: { resourceKey: resourceKey ?? '<descriptor>' },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function serializeColorValueDomain(domain: ColorValueDomain): string {
|
|
50
|
+
if (!isColorValueDomain(domain)) throw invalidDomain(domain);
|
|
51
|
+
return JSON.stringify(domain);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function deserializeColorValueDomain(
|
|
55
|
+
value: unknown,
|
|
56
|
+
): Result<ColorValueDomain, RenderGraphError> {
|
|
57
|
+
let candidate = value;
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
try {
|
|
60
|
+
candidate = JSON.parse(value) as unknown;
|
|
61
|
+
} catch {
|
|
62
|
+
candidate = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return isColorValueDomain(candidate) ? ok(candidate) : err(invalidDomain(candidate));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function serializeColorResourceDescriptor(descriptor: ColorResourceDescriptor): string {
|
|
69
|
+
if (!isColorValueDomain(descriptor.domain)) throw invalidDomain(descriptor.domain);
|
|
70
|
+
return JSON.stringify(descriptor);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function deserializeColorResourceDescriptor(
|
|
74
|
+
value: unknown,
|
|
75
|
+
): Result<ColorResourceDescriptor, RenderGraphError> {
|
|
76
|
+
if (typeof value !== 'object' || value === null) return err(missingDomain());
|
|
77
|
+
const candidate = value as { domain?: unknown; format?: unknown };
|
|
78
|
+
if (candidate.domain === undefined) return err(missingDomain());
|
|
79
|
+
if (!isColorValueDomain(candidate.domain)) return err(invalidDomain(candidate.domain));
|
|
80
|
+
if (typeof candidate.format !== 'string' || candidate.format.length === 0) {
|
|
81
|
+
return err(
|
|
82
|
+
new RenderGraphError({
|
|
83
|
+
code: 'invalid-color-domain',
|
|
84
|
+
expected: 'color resource descriptor includes a non-empty format',
|
|
85
|
+
hint: 'set format separately from the explicit color domain',
|
|
86
|
+
detail: { value: String(candidate.format) },
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return ok({ domain: candidate.domain, format: candidate.format });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function conversionMatches(
|
|
94
|
+
source: ColorValueDomain,
|
|
95
|
+
destination: ColorValueDomain,
|
|
96
|
+
conversion: ColorDomainConversion,
|
|
97
|
+
): boolean {
|
|
98
|
+
if (conversion.kind === 'encode-srgb') {
|
|
99
|
+
return (
|
|
100
|
+
(source === 'linear-hdr' || source === 'linear-ldr') && destination === 'display-encoded'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (conversion.kind === 'decode-srgb') {
|
|
104
|
+
return (
|
|
105
|
+
source === 'display-encoded' && (destination === 'linear-hdr' || destination === 'linear-ldr')
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return (
|
|
109
|
+
source === 'linear-hdr' && (destination === 'linear-ldr' || destination === 'display-encoded')
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function validateColorDomainConnection(
|
|
114
|
+
source: unknown,
|
|
115
|
+
destination: unknown,
|
|
116
|
+
conversion?: ColorDomainConversion,
|
|
117
|
+
): ColorDomainValidation {
|
|
118
|
+
if (source === undefined || source === null) return { ok: false, error: missingDomain('source') };
|
|
119
|
+
if (destination === undefined || destination === null) {
|
|
120
|
+
return { ok: false, error: missingDomain('destination') };
|
|
121
|
+
}
|
|
122
|
+
if (!isColorValueDomain(source)) return { ok: false, error: invalidDomain(source) };
|
|
123
|
+
if (!isColorValueDomain(destination)) return { ok: false, error: invalidDomain(destination) };
|
|
124
|
+
if (source === destination) return { ok: true };
|
|
125
|
+
if (conversion !== undefined && conversionMatches(source, destination, conversion)) {
|
|
126
|
+
return { ok: true };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
error: new RenderGraphError({
|
|
131
|
+
code: 'color-domain-mismatch',
|
|
132
|
+
expected: `source and destination share a domain or use an explicit valid conversion (${source} -> ${destination})`,
|
|
133
|
+
hint: 'insert an explicit linear blend or output encoding pass; never mix into an encoded destination',
|
|
134
|
+
detail: { sourceDomain: source, destinationDomain: destination },
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { invalidDomain, missingDomain };
|