@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
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Buffer,
|
|
3
|
+
RenderPassColorAttachment,
|
|
4
|
+
RenderPassDepthStencilAttachment,
|
|
5
|
+
RhiComputePassEncoder,
|
|
6
|
+
RhiDevice,
|
|
7
|
+
Texture,
|
|
8
|
+
TextureView,
|
|
9
|
+
} from '@forgeax/engine-rhi';
|
|
10
|
+
import { err, ok, RenderGraphError, type Result } from './errors.js';
|
|
11
|
+
import type {
|
|
12
|
+
CompiledPass,
|
|
13
|
+
CompiledResource,
|
|
14
|
+
CompiledView,
|
|
15
|
+
ResourceHandleData,
|
|
16
|
+
} from './kernel-internal.js';
|
|
17
|
+
import { handleData } from './kernel-internal.js';
|
|
18
|
+
import type {
|
|
19
|
+
CompiledRenderGraph,
|
|
20
|
+
CompiledRenderGraphInfo,
|
|
21
|
+
GraphBuffer,
|
|
22
|
+
GraphResourceResolver,
|
|
23
|
+
GraphTexture,
|
|
24
|
+
GraphTextureView,
|
|
25
|
+
RenderGraphFrame,
|
|
26
|
+
RenderGraphPassRunner,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
|
|
29
|
+
interface FrameResources {
|
|
30
|
+
readonly buffers: ReadonlyMap<number, Buffer>;
|
|
31
|
+
readonly textures: ReadonlyMap<number, Texture>;
|
|
32
|
+
readonly views: ReadonlyMap<number, TextureView>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolutionError(label: string, cause?: unknown): RenderGraphError {
|
|
36
|
+
return new RenderGraphError({
|
|
37
|
+
code: 'resource-resolution-failed',
|
|
38
|
+
expected: `resource '${label}' resolves to a live RHI handle for this frame`,
|
|
39
|
+
hint: `repair the imported owner for '${label}' before executing the graph`,
|
|
40
|
+
detail: { resourceLabel: label, ...(cause === undefined ? {} : { accesses: [String(cause)] }) },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
|
|
45
|
+
implements CompiledRenderGraph<FrameCtx>
|
|
46
|
+
{
|
|
47
|
+
private retired = false;
|
|
48
|
+
private retireResult: Promise<Result<void, RenderGraphError>> | undefined;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly generation: number,
|
|
52
|
+
private readonly owner: object,
|
|
53
|
+
private readonly device: RhiDevice,
|
|
54
|
+
private readonly resources: ReadonlyMap<number, CompiledResource<FrameCtx>>,
|
|
55
|
+
private readonly views: ReadonlyMap<number, CompiledView<FrameCtx>>,
|
|
56
|
+
private readonly passes: readonly CompiledPass<FrameCtx>[],
|
|
57
|
+
private readonly info: CompiledRenderGraphInfo,
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
inspect(): CompiledRenderGraphInfo {
|
|
61
|
+
return this.info;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
execute(frame: FrameCtx, runPass?: RenderGraphPassRunner): Result<void, RenderGraphError> {
|
|
65
|
+
if (this.retired) {
|
|
66
|
+
return err(
|
|
67
|
+
new RenderGraphError({
|
|
68
|
+
code: 'compiled-graph-retired',
|
|
69
|
+
expected: 'a compiled graph executes only before retire()',
|
|
70
|
+
hint: 'publish and execute the replacement compiled graph',
|
|
71
|
+
detail: { generation: this.generation },
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resolved = this.resolveFrameResources(frame);
|
|
77
|
+
if (!resolved.ok) return resolved;
|
|
78
|
+
|
|
79
|
+
for (const [executionIndex, pass] of this.passes.entries()) {
|
|
80
|
+
const resolver = this.createPassResolver(pass, resolved.value);
|
|
81
|
+
try {
|
|
82
|
+
if (pass.pass.descriptor.executeIf?.(frame) === false) continue;
|
|
83
|
+
let encodeResult: Result<void, RenderGraphError> = ok(undefined);
|
|
84
|
+
const encode = () => {
|
|
85
|
+
switch (pass.pass.kind) {
|
|
86
|
+
case 'raster': {
|
|
87
|
+
const descriptor = pass.pass.descriptor;
|
|
88
|
+
const colorAttachments: RenderPassColorAttachment[] = [];
|
|
89
|
+
for (const attachment of descriptor.colorAttachments) {
|
|
90
|
+
const view = resolver.textureView(attachment.view);
|
|
91
|
+
if (!view.ok) {
|
|
92
|
+
encodeResult = view;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const resolveTarget =
|
|
96
|
+
attachment.resolveTarget === undefined
|
|
97
|
+
? undefined
|
|
98
|
+
: resolver.textureView(attachment.resolveTarget);
|
|
99
|
+
if (resolveTarget !== undefined && !resolveTarget.ok) {
|
|
100
|
+
encodeResult = resolveTarget;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const clearValue =
|
|
104
|
+
typeof attachment.clearValue === 'function'
|
|
105
|
+
? attachment.clearValue(frame)
|
|
106
|
+
: attachment.clearValue;
|
|
107
|
+
colorAttachments.push({
|
|
108
|
+
view: view.value,
|
|
109
|
+
...(resolveTarget === undefined ? {} : { resolveTarget: resolveTarget.value }),
|
|
110
|
+
...(clearValue === undefined ? {} : { clearValue }),
|
|
111
|
+
loadOp: attachment.loadOp,
|
|
112
|
+
storeOp: attachment.storeOp,
|
|
113
|
+
...(attachment.depthSlice === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: { depthSlice: attachment.depthSlice }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
let depthStencilAttachment: RenderPassDepthStencilAttachment | undefined;
|
|
119
|
+
if (descriptor.depthStencilAttachment !== undefined) {
|
|
120
|
+
const attachment = descriptor.depthStencilAttachment;
|
|
121
|
+
const view = resolver.textureView(attachment.view);
|
|
122
|
+
if (!view.ok) {
|
|
123
|
+
encodeResult = view;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
depthStencilAttachment = {
|
|
127
|
+
view: view.value,
|
|
128
|
+
...(attachment.depthClearValue === undefined
|
|
129
|
+
? {}
|
|
130
|
+
: { depthClearValue: attachment.depthClearValue }),
|
|
131
|
+
...(attachment.depthLoadOp === undefined
|
|
132
|
+
? {}
|
|
133
|
+
: { depthLoadOp: attachment.depthLoadOp }),
|
|
134
|
+
...(attachment.depthStoreOp === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: { depthStoreOp: attachment.depthStoreOp }),
|
|
137
|
+
...(attachment.depthReadOnly === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: { depthReadOnly: attachment.depthReadOnly }),
|
|
140
|
+
...(attachment.stencilClearValue === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { stencilClearValue: attachment.stencilClearValue }),
|
|
143
|
+
...(attachment.stencilLoadOp === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: { stencilLoadOp: attachment.stencilLoadOp }),
|
|
146
|
+
...(attachment.stencilStoreOp === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { stencilStoreOp: attachment.stencilStoreOp }),
|
|
149
|
+
...(attachment.stencilReadOnly === undefined
|
|
150
|
+
? {}
|
|
151
|
+
: { stencilReadOnly: attachment.stencilReadOnly }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const encoder = frame.encoder.beginRenderPass({
|
|
155
|
+
label: pass.name,
|
|
156
|
+
colorAttachments,
|
|
157
|
+
...(depthStencilAttachment === undefined ? {} : { depthStencilAttachment }),
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
descriptor.encode({ pass: encoder, frame, resources: resolver });
|
|
161
|
+
} finally {
|
|
162
|
+
encoder.end();
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case 'compute': {
|
|
167
|
+
const begin = pass.pass.descriptor.begin?.(frame);
|
|
168
|
+
let encoder: RhiComputePassEncoder;
|
|
169
|
+
try {
|
|
170
|
+
encoder = frame.encoder.beginComputePass({ ...(begin ?? {}), label: pass.name });
|
|
171
|
+
} catch (cause) {
|
|
172
|
+
pass.pass.descriptor.onBeginError?.(frame, cause);
|
|
173
|
+
throw cause;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
pass.pass.descriptor.encode({ pass: encoder, frame, resources: resolver });
|
|
177
|
+
} finally {
|
|
178
|
+
encoder.end();
|
|
179
|
+
}
|
|
180
|
+
pass.pass.descriptor.after?.(frame);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 'copy':
|
|
184
|
+
pass.pass.descriptor.encode({ encoder: frame.encoder, frame, resources: resolver });
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
if (runPass === undefined) {
|
|
189
|
+
encode();
|
|
190
|
+
} else {
|
|
191
|
+
runPass({ name: pass.name, kind: pass.pass.kind, executionIndex }, encode);
|
|
192
|
+
}
|
|
193
|
+
if (!encodeResult.ok) return encodeResult;
|
|
194
|
+
} catch (cause) {
|
|
195
|
+
return err(
|
|
196
|
+
new RenderGraphError({
|
|
197
|
+
code: 'pass-encode-failed',
|
|
198
|
+
expected: `pass '${pass.name}' encodes without throwing`,
|
|
199
|
+
hint: 'inspect detail.cause and repair the pass-owned RHI command',
|
|
200
|
+
detail: { passName: pass.name, passKind: pass.pass.kind, cause },
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return ok(undefined);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
retire(): Promise<Result<void, RenderGraphError>> {
|
|
209
|
+
if (this.retireResult !== undefined) return this.retireResult;
|
|
210
|
+
this.retired = true;
|
|
211
|
+
this.retireResult = this.finishRetire();
|
|
212
|
+
return this.retireResult;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async finishRetire(): Promise<Result<void, RenderGraphError>> {
|
|
216
|
+
try {
|
|
217
|
+
await this.device.queue.onSubmittedWorkDone();
|
|
218
|
+
} catch (cause) {
|
|
219
|
+
return err(
|
|
220
|
+
new RenderGraphError({
|
|
221
|
+
code: 'resource-retire-failed',
|
|
222
|
+
expected: 'the GPU submission fence resolves before graph resources retire',
|
|
223
|
+
hint: 'recover the device before retiring the replacement generation',
|
|
224
|
+
detail: { generation: this.generation, cause },
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let firstFailure: RenderGraphError | undefined;
|
|
230
|
+
for (const compiled of this.resources.values()) {
|
|
231
|
+
if (compiled.record.origin === 'imported') continue;
|
|
232
|
+
if (compiled.texture === undefined && compiled.buffer === undefined) continue;
|
|
233
|
+
try {
|
|
234
|
+
const destroyed =
|
|
235
|
+
compiled.record.kind === 'texture'
|
|
236
|
+
? this.device.destroyTexture(compiled.texture as Texture)
|
|
237
|
+
: this.device.destroyBuffer(compiled.buffer as Buffer);
|
|
238
|
+
if (!destroyed.ok && firstFailure === undefined) {
|
|
239
|
+
firstFailure = new RenderGraphError({
|
|
240
|
+
code: 'resource-retire-failed',
|
|
241
|
+
expected: `graph-created resource '${compiled.record.label}' retires exactly once`,
|
|
242
|
+
hint: 'inspect detail.cause for the RHI destroy refusal',
|
|
243
|
+
detail: {
|
|
244
|
+
generation: this.generation,
|
|
245
|
+
resourceLabel: compiled.record.label,
|
|
246
|
+
cause: destroyed.error,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
} catch (cause) {
|
|
251
|
+
firstFailure ??= new RenderGraphError({
|
|
252
|
+
code: 'resource-retire-failed',
|
|
253
|
+
expected: `graph-created resource '${compiled.record.label}' retires exactly once`,
|
|
254
|
+
hint: 'inspect detail.cause for the RHI destroy failure',
|
|
255
|
+
detail: { generation: this.generation, resourceLabel: compiled.record.label, cause },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return firstFailure === undefined ? ok(undefined) : err(firstFailure);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private resolveFrameResources(frame: FrameCtx): Result<FrameResources, RenderGraphError> {
|
|
263
|
+
const buffers = new Map<number, Buffer>();
|
|
264
|
+
const textures = new Map<number, Texture>();
|
|
265
|
+
const views = new Map<number, TextureView>();
|
|
266
|
+
|
|
267
|
+
for (const compiled of this.resources.values()) {
|
|
268
|
+
if (compiled.usage === 0) continue;
|
|
269
|
+
try {
|
|
270
|
+
if (compiled.record.kind === 'texture') {
|
|
271
|
+
const texture =
|
|
272
|
+
compiled.record.origin === 'created'
|
|
273
|
+
? compiled.texture
|
|
274
|
+
: compiled.record.resolve(frame);
|
|
275
|
+
if (texture === undefined) return err(resolutionError(compiled.record.label));
|
|
276
|
+
textures.set(compiled.record.id, texture);
|
|
277
|
+
} else {
|
|
278
|
+
const buffer =
|
|
279
|
+
compiled.record.origin === 'created' ? compiled.buffer : compiled.record.resolve(frame);
|
|
280
|
+
if (buffer === undefined) return err(resolutionError(compiled.record.label));
|
|
281
|
+
buffers.set(compiled.record.id, buffer);
|
|
282
|
+
}
|
|
283
|
+
} catch (cause) {
|
|
284
|
+
return err(resolutionError(compiled.record.label, cause));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const compiledView of this.views.values()) {
|
|
289
|
+
if (this.resources.get(compiledView.record.textureId)?.usage === 0) continue;
|
|
290
|
+
if (compiledView.record.resolve !== undefined) {
|
|
291
|
+
try {
|
|
292
|
+
views.set(compiledView.record.id, compiledView.record.resolve(frame));
|
|
293
|
+
} catch (cause) {
|
|
294
|
+
return err(resolutionError(compiledView.record.label, cause));
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (compiledView.view !== undefined) {
|
|
299
|
+
views.set(compiledView.record.id, compiledView.view);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const texture = textures.get(compiledView.record.textureId);
|
|
303
|
+
if (texture === undefined) return err(resolutionError(compiledView.record.label));
|
|
304
|
+
const created = this.device.createTextureView(texture, compiledView.record.descriptor);
|
|
305
|
+
if (!created.ok) return err(resolutionError(compiledView.record.label, created.error));
|
|
306
|
+
views.set(compiledView.record.id, created.value);
|
|
307
|
+
}
|
|
308
|
+
return ok({ buffers, textures, views });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private createPassResolver(
|
|
312
|
+
pass: CompiledPass<FrameCtx>,
|
|
313
|
+
frameResources: FrameResources,
|
|
314
|
+
): GraphResourceResolver {
|
|
315
|
+
const lookup = (
|
|
316
|
+
resource: GraphBuffer | GraphTexture | GraphTextureView,
|
|
317
|
+
expectedKind: ResourceHandleData['kind'],
|
|
318
|
+
): Result<ResourceHandleData, RenderGraphError> => {
|
|
319
|
+
const data = handleData(resource);
|
|
320
|
+
const resourceId = data?.kind === 'texture-view' ? data.textureId : data?.id;
|
|
321
|
+
if (data === undefined || data.owner !== this.owner || data.kind !== expectedKind) {
|
|
322
|
+
return err(
|
|
323
|
+
new RenderGraphError({
|
|
324
|
+
code: 'foreign-resource-handle',
|
|
325
|
+
expected: `pass '${pass.name}' resolves a handle owned by this compiled graph`,
|
|
326
|
+
hint: 'use only handles created by the builder that declared this pass',
|
|
327
|
+
detail: { passName: pass.name },
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (resourceId === undefined || !pass.resourceIds.has(resourceId)) {
|
|
332
|
+
const label = this.resources.get(resourceId ?? -1)?.record.label;
|
|
333
|
+
return err(
|
|
334
|
+
new RenderGraphError({
|
|
335
|
+
code: 'resource-not-declared-by-pass',
|
|
336
|
+
expected: `pass '${pass.name}' resolves only resources present in its accesses`,
|
|
337
|
+
hint: 'add the resource access to this pass before resolving it',
|
|
338
|
+
detail: { passName: pass.name, resourceLabel: label },
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
if (data.kind === 'texture-view' && !pass.viewIds.has(data.id)) {
|
|
343
|
+
const label = this.views.get(data.id)?.record.label;
|
|
344
|
+
return err(
|
|
345
|
+
new RenderGraphError({
|
|
346
|
+
code: 'resource-not-declared-by-pass',
|
|
347
|
+
expected: `pass '${pass.name}' resolves only texture views present in its accesses`,
|
|
348
|
+
hint: 'add this exact texture view to the pass accesses',
|
|
349
|
+
detail: { passName: pass.name, resourceLabel: label },
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return ok(data);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
buffer: (resource) => {
|
|
358
|
+
const data = lookup(resource, 'buffer');
|
|
359
|
+
if (!data.ok) return data;
|
|
360
|
+
const buffer = frameResources.buffers.get(data.value.id);
|
|
361
|
+
return buffer === undefined
|
|
362
|
+
? err(resolutionError(this.resources.get(data.value.id)?.record.label ?? 'buffer'))
|
|
363
|
+
: ok(buffer);
|
|
364
|
+
},
|
|
365
|
+
texture: (resource) => {
|
|
366
|
+
const data = lookup(resource, 'texture');
|
|
367
|
+
if (!data.ok) return data;
|
|
368
|
+
const texture = frameResources.textures.get(data.value.id);
|
|
369
|
+
return texture === undefined
|
|
370
|
+
? err(resolutionError(this.resources.get(data.value.id)?.record.label ?? 'texture'))
|
|
371
|
+
: ok(texture);
|
|
372
|
+
},
|
|
373
|
+
textureView: (resource) => {
|
|
374
|
+
const data = lookup(resource, 'texture-view');
|
|
375
|
+
if (!data.ok) return data;
|
|
376
|
+
const view = frameResources.views.get(data.value.id);
|
|
377
|
+
return view === undefined
|
|
378
|
+
? err(resolutionError(this.views.get(data.value.id)?.record.label ?? 'texture view'))
|
|
379
|
+
: ok(view);
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @forgeax/engine-render-graph/src/errors.ts — RenderGraphError closed-union
|
|
2
|
+
// error model + Result<T, E>.
|
|
3
|
+
//
|
|
4
|
+
// Shape (plan-strategy D-3):
|
|
5
|
+
// - RenderGraphErrorCode, RenderGraphErrorDetail, and the constructor argument
|
|
6
|
+
// union derive from one private code-to-detail map.
|
|
7
|
+
// - RenderGraphError extends Error { readonly code; readonly expected;
|
|
8
|
+
// readonly hint; readonly detail } — four-field structured error surface,
|
|
9
|
+
// aligned with RhiError (research Finding 8).
|
|
10
|
+
// - RenderGraphErrorDetail is projected from that map; the
|
|
11
|
+
// constructor arguments correlate each code with its accepted detail:
|
|
12
|
+
// - 'dangling-read' / 'unknown-resource' -> { resourceKey, passName }
|
|
13
|
+
// - 'cap-missing' -> { cap, passName }
|
|
14
|
+
// - 'duplicate-resource' -> { resourceKey }
|
|
15
|
+
// - 'alias-source-missing' -> { aliasKey, sourceKey }
|
|
16
|
+
// - Result<T, E=RenderGraphError> = binary tag union ('ok' / 'err') +
|
|
17
|
+
// ok() / err() factories, aligned with RhiError Result (research Finding 8).
|
|
18
|
+
//
|
|
19
|
+
// Related: plan-strategy D-3; research Finding 8; AC-15.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Private code-to-detail authority for the complete RenderGraphError surface.
|
|
23
|
+
*
|
|
24
|
+
* The two resource-key errors intentionally share DanglingReadDetail. Keeping
|
|
25
|
+
* that fact in this map lets the public code and detail unions, plus the
|
|
26
|
+
* constructor arguments, derive from one authority without
|
|
27
|
+
* exporting a framework or runtime registry.
|
|
28
|
+
*/
|
|
29
|
+
interface RenderGraphErrorDetailByCode {
|
|
30
|
+
'dangling-read': DanglingReadDetail;
|
|
31
|
+
'cap-missing': CapMissingDetail;
|
|
32
|
+
'duplicate-resource': DuplicateResourceDetail;
|
|
33
|
+
'alias-source-missing': AliasSourceDetail;
|
|
34
|
+
'unknown-resource': DanglingReadDetail;
|
|
35
|
+
'resource-alloc-failed': ResourceAllocFailedDetail;
|
|
36
|
+
'invalid-format': InvalidFormatDetail;
|
|
37
|
+
'observation-absent': ObservationDetail;
|
|
38
|
+
'observation-invalid-format': ObservationDetail;
|
|
39
|
+
'observation-invalid-size': ObservationDetail;
|
|
40
|
+
'observation-missing-copy-src': ObservationDetail;
|
|
41
|
+
'observation-stale': ObservationDetail;
|
|
42
|
+
'observation-retired': ObservationDetail;
|
|
43
|
+
'invalid-color-domain': ColorDomainDetail;
|
|
44
|
+
'missing-color-domain': ColorDomainDetail;
|
|
45
|
+
'color-domain-mismatch': ColorDomainDetail;
|
|
46
|
+
'duplicate-pass-name': DeclarationDetail;
|
|
47
|
+
'duplicate-resource-label': DeclarationDetail;
|
|
48
|
+
'builder-sealed': DeclarationDetail;
|
|
49
|
+
'foreign-resource-handle': ResourceAccessDetail;
|
|
50
|
+
'resource-not-declared-by-pass': ResourceAccessDetail;
|
|
51
|
+
'uninitialized-read': ResourceAccessDetail;
|
|
52
|
+
'access-conflict': ResourceAccessDetail;
|
|
53
|
+
'capability-missing': CapabilityDetail;
|
|
54
|
+
'resource-descriptor-invalid': DescriptorDetail;
|
|
55
|
+
'import-usage-mismatch': DescriptorDetail;
|
|
56
|
+
'resource-allocation-failed': ResourceAllocFailedDetail;
|
|
57
|
+
'resource-resolution-failed': ResourceAccessDetail;
|
|
58
|
+
'pass-encode-failed': PassFailureDetail;
|
|
59
|
+
'compiled-graph-retired': LifecycleDetail;
|
|
60
|
+
'resource-retire-failed': LifecycleDetail;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Closed RenderGraphErrorCode union derived from the private detail map. */
|
|
64
|
+
export type RenderGraphErrorCode = keyof RenderGraphErrorDetailByCode;
|
|
65
|
+
|
|
66
|
+
/** Detail union projected from the private code-to-detail map. */
|
|
67
|
+
export type RenderGraphErrorDetail = RenderGraphErrorDetailByCode[RenderGraphErrorCode];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Constructor arguments correlated by code. `detail` remains optional to
|
|
71
|
+
* preserve the existing envelope behavior for callers that only need the
|
|
72
|
+
* top-level fields.
|
|
73
|
+
*/
|
|
74
|
+
type RenderGraphErrorConstructorArgs = {
|
|
75
|
+
[Code in RenderGraphErrorCode]: {
|
|
76
|
+
code: Code;
|
|
77
|
+
expected: string;
|
|
78
|
+
hint: string;
|
|
79
|
+
detail?: RenderGraphErrorDetailByCode[Code] | undefined;
|
|
80
|
+
};
|
|
81
|
+
}[RenderGraphErrorCode];
|
|
82
|
+
|
|
83
|
+
/** Detail variant for dangling-read and unknown-resource errors. */
|
|
84
|
+
export interface DanglingReadDetail {
|
|
85
|
+
readonly resourceKey: string;
|
|
86
|
+
readonly passName: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Detail variant for cap-missing errors. */
|
|
90
|
+
export interface CapMissingDetail {
|
|
91
|
+
readonly cap: 'compute' | 'storageBuffer';
|
|
92
|
+
readonly passName: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Detail variant for duplicate-resource errors. */
|
|
96
|
+
export interface DuplicateResourceDetail {
|
|
97
|
+
readonly resourceKey: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Detail variant for aliases whose source is not a registered color target. */
|
|
101
|
+
export interface AliasSourceDetail {
|
|
102
|
+
readonly aliasKey: string;
|
|
103
|
+
readonly sourceKey: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Detail variant for resource-alloc-failed errors. */
|
|
107
|
+
export interface ResourceAllocFailedDetail {
|
|
108
|
+
readonly resourceKey: string;
|
|
109
|
+
readonly passName?: string | undefined;
|
|
110
|
+
readonly rhiCode?: string | undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Detail variant for invalid-format errors. */
|
|
114
|
+
export interface InvalidFormatDetail {
|
|
115
|
+
readonly resourceKey: string;
|
|
116
|
+
readonly format: string;
|
|
117
|
+
readonly expected: readonly string[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Generic current-frame observation failures. */
|
|
121
|
+
export interface ObservationDetail {
|
|
122
|
+
readonly frameId?: number | undefined;
|
|
123
|
+
readonly expected?: string | undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ColorDomainDetail {
|
|
127
|
+
readonly value?: string | undefined;
|
|
128
|
+
readonly resourceKey?: string | undefined;
|
|
129
|
+
readonly sourceDomain?: string | undefined;
|
|
130
|
+
readonly destinationDomain?: string | undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface DeclarationDetail {
|
|
134
|
+
readonly passName?: string | undefined;
|
|
135
|
+
readonly resourceLabel?: string | undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ResourceAccessDetail {
|
|
139
|
+
readonly passName?: string | undefined;
|
|
140
|
+
readonly resourceLabel?: string | undefined;
|
|
141
|
+
readonly usage?: string | undefined;
|
|
142
|
+
readonly accesses?: readonly string[] | undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface CapabilityDetail extends ResourceAccessDetail {
|
|
146
|
+
readonly capability: 'compute' | 'storage-buffer' | 'storage-texture' | 'indirect';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface DescriptorDetail {
|
|
150
|
+
readonly resourceLabel: string;
|
|
151
|
+
readonly field: string;
|
|
152
|
+
readonly expected: string;
|
|
153
|
+
readonly actual?: string | number | undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface PassFailureDetail {
|
|
157
|
+
readonly passName: string;
|
|
158
|
+
readonly passKind: 'raster' | 'compute' | 'copy';
|
|
159
|
+
readonly cause: unknown;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface LifecycleDetail {
|
|
163
|
+
readonly generation: number;
|
|
164
|
+
readonly resourceLabel?: string | undefined;
|
|
165
|
+
readonly cause?: unknown;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Structured RenderGraph error.
|
|
170
|
+
*
|
|
171
|
+
* Four readonly fields aligned with AGENTS.md "Errors are structured"
|
|
172
|
+
* and RhiError (research Finding 8):
|
|
173
|
+
* - `.code` — closed union member (L1 key signal).
|
|
174
|
+
* - `.expected` — expected-state description (L2 detail).
|
|
175
|
+
* - `.hint` — actionable recovery guidance (L2 detail).
|
|
176
|
+
* - `.detail` — narrowed payload per code variant.
|
|
177
|
+
*/
|
|
178
|
+
export class RenderGraphError extends Error {
|
|
179
|
+
readonly code: RenderGraphErrorCode;
|
|
180
|
+
readonly expected: string;
|
|
181
|
+
readonly hint: string;
|
|
182
|
+
readonly detail: RenderGraphErrorDetail | undefined;
|
|
183
|
+
|
|
184
|
+
constructor(args: RenderGraphErrorConstructorArgs) {
|
|
185
|
+
super(`[RenderGraphError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);
|
|
186
|
+
this.name = 'RenderGraphError';
|
|
187
|
+
this.code = args.code;
|
|
188
|
+
this.expected = args.expected;
|
|
189
|
+
this.hint = args.hint;
|
|
190
|
+
this.detail = args.detail;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Result<T, E> + ok / err + ResultOk / ResultErr live in `@forgeax/engine-types`
|
|
195
|
+
// (tweak-20260612-result-into-types). Consolidated upstream from this and 4
|
|
196
|
+
// other packages' duplicate definitions; the barrel here re-exports them so
|
|
197
|
+
// existing `import { err, ok, Result } from '@forgeax/engine-render-graph'`
|
|
198
|
+
// consumers stay unchanged.
|
|
199
|
+
export {
|
|
200
|
+
err,
|
|
201
|
+
ok,
|
|
202
|
+
type Result,
|
|
203
|
+
type ResultErr,
|
|
204
|
+
type ResultOk,
|
|
205
|
+
} from '@forgeax/engine-types';
|