@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.
Files changed (66) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +184 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/observation.unit.test.d.ts +2 -0
  5. package/dist/__tests__/observation.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/render-graph-alias-source.unit.test.d.ts +2 -0
  7. package/dist/__tests__/render-graph-alias-source.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/render-graph-builder.unit.test.d.ts +2 -0
  9. package/dist/__tests__/render-graph-builder.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/render-graph-errors.test-d.d.ts +2 -0
  11. package/dist/__tests__/render-graph-errors.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/render-graph-regressions.unit.test.d.ts +2 -0
  13. package/dist/__tests__/render-graph-regressions.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/render-graph-rhi-null.integration.test.d.ts +2 -0
  15. package/dist/__tests__/render-graph-rhi-null.integration.test.d.ts.map +1 -0
  16. package/dist/__tests__/render-graph.unit.test.d.ts +2 -0
  17. package/dist/__tests__/render-graph.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/resource-declaration-collision.test.d.ts +2 -0
  19. package/dist/__tests__/resource-declaration-collision.test.d.ts.map +1 -0
  20. package/dist/builder.d.ts +42 -0
  21. package/dist/builder.d.ts.map +1 -0
  22. package/dist/compiled-graph.d.ts +23 -0
  23. package/dist/compiled-graph.d.ts.map +1 -0
  24. package/dist/errors.d.ts +148 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/graph.d.ts +403 -0
  27. package/dist/graph.d.ts.map +1 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.mjs +2256 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/dist/kernel-internal.d.ts +84 -0
  33. package/dist/kernel-internal.d.ts.map +1 -0
  34. package/dist/observation.d.ts +38 -0
  35. package/dist/observation.d.ts.map +1 -0
  36. package/dist/pass-registry.d.ts +12 -0
  37. package/dist/pass-registry.d.ts.map +1 -0
  38. package/dist/pipeline/__tests__/color-value-domain.test.d.ts +2 -0
  39. package/dist/pipeline/__tests__/color-value-domain.test.d.ts.map +1 -0
  40. package/dist/pipeline/color-value-domain.d.ts +35 -0
  41. package/dist/pipeline/color-value-domain.d.ts.map +1 -0
  42. package/dist/resource-registry.d.ts +62 -0
  43. package/dist/resource-registry.d.ts.map +1 -0
  44. package/dist/types.d.ts +171 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/package.json +61 -0
  47. package/src/__tests__/observation.unit.test.ts +103 -0
  48. package/src/__tests__/render-graph-alias-source.unit.test.ts +118 -0
  49. package/src/__tests__/render-graph-builder.unit.test.ts +673 -0
  50. package/src/__tests__/render-graph-errors.test-d.ts +207 -0
  51. package/src/__tests__/render-graph-regressions.unit.test.ts +126 -0
  52. package/src/__tests__/render-graph-rhi-null.integration.test.ts +86 -0
  53. package/src/__tests__/render-graph.unit.test.ts +2551 -0
  54. package/src/__tests__/resource-declaration-collision.test.ts +151 -0
  55. package/src/builder.ts +1009 -0
  56. package/src/compiled-graph.ts +383 -0
  57. package/src/errors.ts +205 -0
  58. package/src/graph.ts +1269 -0
  59. package/src/index.ts +97 -0
  60. package/src/kernel-internal.ts +148 -0
  61. package/src/observation.ts +134 -0
  62. package/src/pass-registry.ts +43 -0
  63. package/src/pipeline/__tests__/color-value-domain.test.ts +43 -0
  64. package/src/pipeline/color-value-domain.ts +139 -0
  65. package/src/resource-registry.ts +174 -0
  66. package/src/types.ts +216 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,2256 @@
1
+ import { ok, err } from '@forgeax/engine-types';
2
+ export { err, ok } from '@forgeax/engine-types';
3
+
4
+ // src/errors.ts
5
+ var RenderGraphError = class extends Error {
6
+ code;
7
+ expected;
8
+ hint;
9
+ detail;
10
+ constructor(args) {
11
+ super(`[RenderGraphError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);
12
+ this.name = "RenderGraphError";
13
+ this.code = args.code;
14
+ this.expected = args.expected;
15
+ this.hint = args.hint;
16
+ this.detail = args.detail;
17
+ }
18
+ };
19
+
20
+ // src/kernel-internal.ts
21
+ function textureHandle(owner, id) {
22
+ return Object.freeze({ owner, id, kind: "texture" });
23
+ }
24
+ function textureViewHandle(owner, id, textureId) {
25
+ return Object.freeze({
26
+ owner,
27
+ id,
28
+ kind: "texture-view",
29
+ textureId
30
+ });
31
+ }
32
+ function bufferHandle(owner, id) {
33
+ return Object.freeze({ owner, id, kind: "buffer" });
34
+ }
35
+ function handleData(resource) {
36
+ if (typeof resource !== "object" || resource === null) return void 0;
37
+ const candidate = resource;
38
+ if (typeof candidate.id !== "number" || typeof candidate.owner !== "object") return void 0;
39
+ if (candidate.kind !== "texture" && candidate.kind !== "texture-view" && candidate.kind !== "buffer") {
40
+ return void 0;
41
+ }
42
+ return candidate;
43
+ }
44
+ function accessResourceId(access) {
45
+ const data = handleData(access.resource);
46
+ if (data?.kind === "texture-view") return data.textureId;
47
+ return data?.id;
48
+ }
49
+
50
+ // src/compiled-graph.ts
51
+ function resolutionError(label, cause) {
52
+ return new RenderGraphError({
53
+ code: "resource-resolution-failed",
54
+ expected: `resource '${label}' resolves to a live RHI handle for this frame`,
55
+ hint: `repair the imported owner for '${label}' before executing the graph`,
56
+ detail: { resourceLabel: label, ...cause === void 0 ? {} : { accesses: [String(cause)] } }
57
+ });
58
+ }
59
+ var CompiledRenderGraphImpl = class {
60
+ constructor(generation, owner, device, resources, views, passes, info) {
61
+ this.generation = generation;
62
+ this.owner = owner;
63
+ this.device = device;
64
+ this.resources = resources;
65
+ this.views = views;
66
+ this.passes = passes;
67
+ this.info = info;
68
+ }
69
+ generation;
70
+ owner;
71
+ device;
72
+ resources;
73
+ views;
74
+ passes;
75
+ info;
76
+ retired = false;
77
+ retireResult;
78
+ inspect() {
79
+ return this.info;
80
+ }
81
+ execute(frame, runPass) {
82
+ if (this.retired) {
83
+ return err(
84
+ new RenderGraphError({
85
+ code: "compiled-graph-retired",
86
+ expected: "a compiled graph executes only before retire()",
87
+ hint: "publish and execute the replacement compiled graph",
88
+ detail: { generation: this.generation }
89
+ })
90
+ );
91
+ }
92
+ const resolved = this.resolveFrameResources(frame);
93
+ if (!resolved.ok) return resolved;
94
+ for (const [executionIndex, pass] of this.passes.entries()) {
95
+ const resolver = this.createPassResolver(pass, resolved.value);
96
+ try {
97
+ if (pass.pass.descriptor.executeIf?.(frame) === false) continue;
98
+ let encodeResult = ok(void 0);
99
+ const encode = () => {
100
+ switch (pass.pass.kind) {
101
+ case "raster": {
102
+ const descriptor = pass.pass.descriptor;
103
+ const colorAttachments = [];
104
+ for (const attachment of descriptor.colorAttachments) {
105
+ const view = resolver.textureView(attachment.view);
106
+ if (!view.ok) {
107
+ encodeResult = view;
108
+ return;
109
+ }
110
+ const resolveTarget = attachment.resolveTarget === void 0 ? void 0 : resolver.textureView(attachment.resolveTarget);
111
+ if (resolveTarget !== void 0 && !resolveTarget.ok) {
112
+ encodeResult = resolveTarget;
113
+ return;
114
+ }
115
+ const clearValue = typeof attachment.clearValue === "function" ? attachment.clearValue(frame) : attachment.clearValue;
116
+ colorAttachments.push({
117
+ view: view.value,
118
+ ...resolveTarget === void 0 ? {} : { resolveTarget: resolveTarget.value },
119
+ ...clearValue === void 0 ? {} : { clearValue },
120
+ loadOp: attachment.loadOp,
121
+ storeOp: attachment.storeOp,
122
+ ...attachment.depthSlice === void 0 ? {} : { depthSlice: attachment.depthSlice }
123
+ });
124
+ }
125
+ let depthStencilAttachment;
126
+ if (descriptor.depthStencilAttachment !== void 0) {
127
+ const attachment = descriptor.depthStencilAttachment;
128
+ const view = resolver.textureView(attachment.view);
129
+ if (!view.ok) {
130
+ encodeResult = view;
131
+ return;
132
+ }
133
+ depthStencilAttachment = {
134
+ view: view.value,
135
+ ...attachment.depthClearValue === void 0 ? {} : { depthClearValue: attachment.depthClearValue },
136
+ ...attachment.depthLoadOp === void 0 ? {} : { depthLoadOp: attachment.depthLoadOp },
137
+ ...attachment.depthStoreOp === void 0 ? {} : { depthStoreOp: attachment.depthStoreOp },
138
+ ...attachment.depthReadOnly === void 0 ? {} : { depthReadOnly: attachment.depthReadOnly },
139
+ ...attachment.stencilClearValue === void 0 ? {} : { stencilClearValue: attachment.stencilClearValue },
140
+ ...attachment.stencilLoadOp === void 0 ? {} : { stencilLoadOp: attachment.stencilLoadOp },
141
+ ...attachment.stencilStoreOp === void 0 ? {} : { stencilStoreOp: attachment.stencilStoreOp },
142
+ ...attachment.stencilReadOnly === void 0 ? {} : { stencilReadOnly: attachment.stencilReadOnly }
143
+ };
144
+ }
145
+ const encoder = frame.encoder.beginRenderPass({
146
+ label: pass.name,
147
+ colorAttachments,
148
+ ...depthStencilAttachment === void 0 ? {} : { depthStencilAttachment }
149
+ });
150
+ try {
151
+ descriptor.encode({ pass: encoder, frame, resources: resolver });
152
+ } finally {
153
+ encoder.end();
154
+ }
155
+ break;
156
+ }
157
+ case "compute": {
158
+ const begin = pass.pass.descriptor.begin?.(frame);
159
+ let encoder;
160
+ try {
161
+ encoder = frame.encoder.beginComputePass({ ...begin ?? {}, label: pass.name });
162
+ } catch (cause) {
163
+ pass.pass.descriptor.onBeginError?.(frame, cause);
164
+ throw cause;
165
+ }
166
+ try {
167
+ pass.pass.descriptor.encode({ pass: encoder, frame, resources: resolver });
168
+ } finally {
169
+ encoder.end();
170
+ }
171
+ pass.pass.descriptor.after?.(frame);
172
+ break;
173
+ }
174
+ case "copy":
175
+ pass.pass.descriptor.encode({ encoder: frame.encoder, frame, resources: resolver });
176
+ break;
177
+ }
178
+ };
179
+ if (runPass === void 0) {
180
+ encode();
181
+ } else {
182
+ runPass({ name: pass.name, kind: pass.pass.kind, executionIndex }, encode);
183
+ }
184
+ if (!encodeResult.ok) return encodeResult;
185
+ } catch (cause) {
186
+ return err(
187
+ new RenderGraphError({
188
+ code: "pass-encode-failed",
189
+ expected: `pass '${pass.name}' encodes without throwing`,
190
+ hint: "inspect detail.cause and repair the pass-owned RHI command",
191
+ detail: { passName: pass.name, passKind: pass.pass.kind, cause }
192
+ })
193
+ );
194
+ }
195
+ }
196
+ return ok(void 0);
197
+ }
198
+ retire() {
199
+ if (this.retireResult !== void 0) return this.retireResult;
200
+ this.retired = true;
201
+ this.retireResult = this.finishRetire();
202
+ return this.retireResult;
203
+ }
204
+ async finishRetire() {
205
+ try {
206
+ await this.device.queue.onSubmittedWorkDone();
207
+ } catch (cause) {
208
+ return err(
209
+ new RenderGraphError({
210
+ code: "resource-retire-failed",
211
+ expected: "the GPU submission fence resolves before graph resources retire",
212
+ hint: "recover the device before retiring the replacement generation",
213
+ detail: { generation: this.generation, cause }
214
+ })
215
+ );
216
+ }
217
+ let firstFailure;
218
+ for (const compiled of this.resources.values()) {
219
+ if (compiled.record.origin === "imported") continue;
220
+ if (compiled.texture === void 0 && compiled.buffer === void 0) continue;
221
+ try {
222
+ const destroyed = compiled.record.kind === "texture" ? this.device.destroyTexture(compiled.texture) : this.device.destroyBuffer(compiled.buffer);
223
+ if (!destroyed.ok && firstFailure === void 0) {
224
+ firstFailure = new RenderGraphError({
225
+ code: "resource-retire-failed",
226
+ expected: `graph-created resource '${compiled.record.label}' retires exactly once`,
227
+ hint: "inspect detail.cause for the RHI destroy refusal",
228
+ detail: {
229
+ generation: this.generation,
230
+ resourceLabel: compiled.record.label,
231
+ cause: destroyed.error
232
+ }
233
+ });
234
+ }
235
+ } catch (cause) {
236
+ firstFailure ??= new RenderGraphError({
237
+ code: "resource-retire-failed",
238
+ expected: `graph-created resource '${compiled.record.label}' retires exactly once`,
239
+ hint: "inspect detail.cause for the RHI destroy failure",
240
+ detail: { generation: this.generation, resourceLabel: compiled.record.label, cause }
241
+ });
242
+ }
243
+ }
244
+ return firstFailure === void 0 ? ok(void 0) : err(firstFailure);
245
+ }
246
+ resolveFrameResources(frame) {
247
+ const buffers = /* @__PURE__ */ new Map();
248
+ const textures = /* @__PURE__ */ new Map();
249
+ const views = /* @__PURE__ */ new Map();
250
+ for (const compiled of this.resources.values()) {
251
+ if (compiled.usage === 0) continue;
252
+ try {
253
+ if (compiled.record.kind === "texture") {
254
+ const texture = compiled.record.origin === "created" ? compiled.texture : compiled.record.resolve(frame);
255
+ if (texture === void 0) return err(resolutionError(compiled.record.label));
256
+ textures.set(compiled.record.id, texture);
257
+ } else {
258
+ const buffer = compiled.record.origin === "created" ? compiled.buffer : compiled.record.resolve(frame);
259
+ if (buffer === void 0) return err(resolutionError(compiled.record.label));
260
+ buffers.set(compiled.record.id, buffer);
261
+ }
262
+ } catch (cause) {
263
+ return err(resolutionError(compiled.record.label, cause));
264
+ }
265
+ }
266
+ for (const compiledView of this.views.values()) {
267
+ if (this.resources.get(compiledView.record.textureId)?.usage === 0) continue;
268
+ if (compiledView.record.resolve !== void 0) {
269
+ try {
270
+ views.set(compiledView.record.id, compiledView.record.resolve(frame));
271
+ } catch (cause) {
272
+ return err(resolutionError(compiledView.record.label, cause));
273
+ }
274
+ continue;
275
+ }
276
+ if (compiledView.view !== void 0) {
277
+ views.set(compiledView.record.id, compiledView.view);
278
+ continue;
279
+ }
280
+ const texture = textures.get(compiledView.record.textureId);
281
+ if (texture === void 0) return err(resolutionError(compiledView.record.label));
282
+ const created = this.device.createTextureView(texture, compiledView.record.descriptor);
283
+ if (!created.ok) return err(resolutionError(compiledView.record.label, created.error));
284
+ views.set(compiledView.record.id, created.value);
285
+ }
286
+ return ok({ buffers, textures, views });
287
+ }
288
+ createPassResolver(pass, frameResources) {
289
+ const lookup = (resource, expectedKind) => {
290
+ const data = handleData(resource);
291
+ const resourceId = data?.kind === "texture-view" ? data.textureId : data?.id;
292
+ if (data === void 0 || data.owner !== this.owner || data.kind !== expectedKind) {
293
+ return err(
294
+ new RenderGraphError({
295
+ code: "foreign-resource-handle",
296
+ expected: `pass '${pass.name}' resolves a handle owned by this compiled graph`,
297
+ hint: "use only handles created by the builder that declared this pass",
298
+ detail: { passName: pass.name }
299
+ })
300
+ );
301
+ }
302
+ if (resourceId === void 0 || !pass.resourceIds.has(resourceId)) {
303
+ const label = this.resources.get(resourceId ?? -1)?.record.label;
304
+ return err(
305
+ new RenderGraphError({
306
+ code: "resource-not-declared-by-pass",
307
+ expected: `pass '${pass.name}' resolves only resources present in its accesses`,
308
+ hint: "add the resource access to this pass before resolving it",
309
+ detail: { passName: pass.name, resourceLabel: label }
310
+ })
311
+ );
312
+ }
313
+ if (data.kind === "texture-view" && !pass.viewIds.has(data.id)) {
314
+ const label = this.views.get(data.id)?.record.label;
315
+ return err(
316
+ new RenderGraphError({
317
+ code: "resource-not-declared-by-pass",
318
+ expected: `pass '${pass.name}' resolves only texture views present in its accesses`,
319
+ hint: "add this exact texture view to the pass accesses",
320
+ detail: { passName: pass.name, resourceLabel: label }
321
+ })
322
+ );
323
+ }
324
+ return ok(data);
325
+ };
326
+ return {
327
+ buffer: (resource) => {
328
+ const data = lookup(resource, "buffer");
329
+ if (!data.ok) return data;
330
+ const buffer = frameResources.buffers.get(data.value.id);
331
+ return buffer === void 0 ? err(resolutionError(this.resources.get(data.value.id)?.record.label ?? "buffer")) : ok(buffer);
332
+ },
333
+ texture: (resource) => {
334
+ const data = lookup(resource, "texture");
335
+ if (!data.ok) return data;
336
+ const texture = frameResources.textures.get(data.value.id);
337
+ return texture === void 0 ? err(resolutionError(this.resources.get(data.value.id)?.record.label ?? "texture")) : ok(texture);
338
+ },
339
+ textureView: (resource) => {
340
+ const data = lookup(resource, "texture-view");
341
+ if (!data.ok) return data;
342
+ const view = frameResources.views.get(data.value.id);
343
+ return view === void 0 ? err(resolutionError(this.views.get(data.value.id)?.record.label ?? "texture view")) : ok(view);
344
+ }
345
+ };
346
+ }
347
+ };
348
+
349
+ // src/builder.ts
350
+ var BUFFER_USAGE = {
351
+ copySrc: 4,
352
+ copyDst: 8,
353
+ index: 16,
354
+ vertex: 32,
355
+ uniform: 64,
356
+ storage: 128,
357
+ indirect: 256
358
+ };
359
+ var TEXTURE_USAGE = {
360
+ copySrc: 1,
361
+ copyDst: 2,
362
+ textureBinding: 4,
363
+ storageBinding: 8,
364
+ renderAttachment: 16
365
+ };
366
+ var nextGeneration = 1;
367
+ function bufferUsage(access) {
368
+ switch (access) {
369
+ case "uniform-read":
370
+ return BUFFER_USAGE.uniform;
371
+ case "storage-read":
372
+ case "storage-write":
373
+ case "storage-read-write":
374
+ return BUFFER_USAGE.storage;
375
+ case "indirect-read":
376
+ return BUFFER_USAGE.indirect;
377
+ case "vertex-read":
378
+ return BUFFER_USAGE.vertex;
379
+ case "index-read":
380
+ return BUFFER_USAGE.index;
381
+ case "copy-src":
382
+ return BUFFER_USAGE.copySrc;
383
+ case "copy-dst":
384
+ return BUFFER_USAGE.copyDst;
385
+ }
386
+ }
387
+ function textureUsage(access) {
388
+ switch (access) {
389
+ case "sampled-read":
390
+ return TEXTURE_USAGE.textureBinding;
391
+ case "storage-read":
392
+ case "storage-write":
393
+ case "storage-read-write":
394
+ return TEXTURE_USAGE.storageBinding;
395
+ case "color-attachment":
396
+ case "depth-stencil-read":
397
+ case "depth-stencil-write":
398
+ return TEXTURE_USAGE.renderAttachment;
399
+ case "copy-src":
400
+ return TEXTURE_USAGE.copySrc;
401
+ case "copy-dst":
402
+ return TEXTURE_USAGE.copyDst;
403
+ }
404
+ }
405
+ function accessMode(access) {
406
+ switch (access) {
407
+ case "storage-read-write":
408
+ return { read: true, write: true };
409
+ case "storage-write":
410
+ case "color-attachment":
411
+ case "depth-stencil-write":
412
+ case "copy-dst":
413
+ return { read: false, write: true };
414
+ default:
415
+ return { read: true, write: false };
416
+ }
417
+ }
418
+ function rangesOverlap(left, right) {
419
+ if (left === void 0 || right === void 0) return true;
420
+ const aspectOverlap = left.aspect === "all" || right.aspect === "all" || left.aspect === right.aspect;
421
+ return aspectOverlap && left.mipStart < right.mipEnd && right.mipStart < left.mipEnd && left.layerStart < right.layerEnd && right.layerStart < left.layerEnd;
422
+ }
423
+ function freezeInfo(info) {
424
+ return Object.freeze({
425
+ passes: Object.freeze(
426
+ info.passes.map(
427
+ (pass) => Object.freeze({
428
+ ...pass,
429
+ accesses: Object.freeze(pass.accesses.map((access) => Object.freeze({ ...access }))),
430
+ dependencies: Object.freeze([...pass.dependencies])
431
+ })
432
+ )
433
+ ),
434
+ resources: Object.freeze(info.resources.map((resource) => Object.freeze({ ...resource })))
435
+ });
436
+ }
437
+ var RenderGraphBuilder = class {
438
+ owner = Object.freeze({});
439
+ labels = /* @__PURE__ */ new Set();
440
+ resources = /* @__PURE__ */ new Map();
441
+ views = /* @__PURE__ */ new Map();
442
+ passes = [];
443
+ passNames = /* @__PURE__ */ new Set();
444
+ nextId = 1;
445
+ sealed = false;
446
+ createTexture(label, descriptor) {
447
+ const writable = this.ensureWritable();
448
+ if (!writable.ok) return writable;
449
+ const unique = this.reserveLabel(label);
450
+ if (!unique.ok) return unique;
451
+ const id = this.nextId++;
452
+ this.resources.set(id, { id, label, kind: "texture", origin: "created", descriptor });
453
+ return ok(textureHandle(this.owner, id));
454
+ }
455
+ importTexture(label, descriptor, resolve) {
456
+ const writable = this.ensureWritable();
457
+ if (!writable.ok) return writable;
458
+ const unique = this.reserveLabel(label);
459
+ if (!unique.ok) return unique;
460
+ const id = this.nextId++;
461
+ this.resources.set(id, {
462
+ id,
463
+ label,
464
+ kind: "texture",
465
+ origin: "imported",
466
+ descriptor,
467
+ resolve
468
+ });
469
+ return ok(textureHandle(this.owner, id));
470
+ }
471
+ createBuffer(label, descriptor) {
472
+ const writable = this.ensureWritable();
473
+ if (!writable.ok) return writable;
474
+ const unique = this.reserveLabel(label);
475
+ if (!unique.ok) return unique;
476
+ const id = this.nextId++;
477
+ this.resources.set(id, { id, label, kind: "buffer", origin: "created", descriptor });
478
+ return ok(bufferHandle(this.owner, id));
479
+ }
480
+ importBuffer(label, descriptor, resolve) {
481
+ const writable = this.ensureWritable();
482
+ if (!writable.ok) return writable;
483
+ const unique = this.reserveLabel(label);
484
+ if (!unique.ok) return unique;
485
+ const id = this.nextId++;
486
+ this.resources.set(id, {
487
+ id,
488
+ label,
489
+ kind: "buffer",
490
+ origin: "imported",
491
+ descriptor,
492
+ resolve
493
+ });
494
+ return ok(bufferHandle(this.owner, id));
495
+ }
496
+ view(texture, descriptor = {}) {
497
+ const writable = this.ensureWritable();
498
+ if (!writable.ok) return writable;
499
+ const data = handleData(texture);
500
+ if (data?.kind !== "texture" || data.owner !== this.owner) {
501
+ return err(this.foreignHandleError());
502
+ }
503
+ const textureRecord = this.resources.get(data.id);
504
+ if (textureRecord?.kind !== "texture") return err(this.foreignHandleError());
505
+ const label = descriptor.label ?? `${textureRecord.label}.view.${this.nextId}`;
506
+ const unique = this.reserveLabel(label);
507
+ if (!unique.ok) return unique;
508
+ const id = this.nextId++;
509
+ this.views.set(id, { id, label, textureId: data.id, descriptor });
510
+ return ok(textureViewHandle(this.owner, id, data.id));
511
+ }
512
+ importView(texture, descriptor, resolve) {
513
+ const writable = this.ensureWritable();
514
+ if (!writable.ok) return writable;
515
+ const data = handleData(texture);
516
+ if (data?.kind !== "texture" || data.owner !== this.owner) {
517
+ return err(this.foreignHandleError());
518
+ }
519
+ const textureRecord = this.resources.get(data.id);
520
+ if (textureRecord?.kind !== "texture" || textureRecord.origin !== "imported") {
521
+ return err(
522
+ new RenderGraphError({
523
+ code: "resource-descriptor-invalid",
524
+ expected: "an imported view belongs to an imported texture",
525
+ hint: "use view() for graph-created textures and importView() for host-owned views",
526
+ detail: {
527
+ resourceLabel: textureRecord?.label ?? "foreign",
528
+ field: "origin",
529
+ expected: "imported",
530
+ actual: textureRecord?.origin ?? "foreign"
531
+ }
532
+ })
533
+ );
534
+ }
535
+ const label = descriptor.label ?? `${textureRecord.label}.view.${this.nextId}`;
536
+ const unique = this.reserveLabel(label);
537
+ if (!unique.ok) return unique;
538
+ const id = this.nextId++;
539
+ this.views.set(id, { id, label, textureId: data.id, descriptor, resolve });
540
+ return ok(textureViewHandle(this.owner, id, data.id));
541
+ }
542
+ addRasterPass(name, descriptor) {
543
+ return this.addPass(name, { kind: "raster", descriptor });
544
+ }
545
+ addComputePass(name, descriptor) {
546
+ return this.addPass(name, { kind: "compute", descriptor });
547
+ }
548
+ addCopyPass(name, descriptor) {
549
+ return this.addPass(name, { kind: "copy", descriptor });
550
+ }
551
+ compile(options) {
552
+ const writable = this.ensureWritable();
553
+ if (!writable.ok) return writable;
554
+ this.sealed = true;
555
+ const descriptors = this.validateDescriptors(options.surfaceSize);
556
+ if (!descriptors.ok) return descriptors;
557
+ const analyzed = this.analyze(options.device.caps);
558
+ if (!analyzed.ok) return analyzed;
559
+ const allocated = this.allocate(
560
+ options,
561
+ analyzed.value.usageByResource,
562
+ analyzed.value.firstUseByResource,
563
+ analyzed.value.lastUseByResource
564
+ );
565
+ if (!allocated.ok) return allocated;
566
+ const info = freezeInfo({
567
+ passes: analyzed.value.passes.map((pass, executionIndex) => ({
568
+ name: pass.name,
569
+ kind: pass.pass.kind,
570
+ executionIndex,
571
+ accesses: pass.pass.descriptor.accesses.map((access) => {
572
+ const id = accessResourceId(access);
573
+ return {
574
+ resource: this.resources.get(id ?? -1)?.label ?? "foreign",
575
+ usage: access.usage
576
+ };
577
+ }),
578
+ dependencies: pass.dependencies.map(
579
+ (dependency) => analyzed.value.passes[dependency]?.name ?? "unknown"
580
+ )
581
+ })),
582
+ resources: [...allocated.value.resources.values()].map((resource) => ({
583
+ label: resource.record.label,
584
+ kind: resource.record.kind,
585
+ origin: resource.record.origin,
586
+ firstUse: resource.firstUse,
587
+ lastUse: resource.lastUse,
588
+ derivedUsage: resource.usage
589
+ }))
590
+ });
591
+ return ok(
592
+ new CompiledRenderGraphImpl(
593
+ nextGeneration++,
594
+ this.owner,
595
+ options.device,
596
+ allocated.value.resources,
597
+ allocated.value.views,
598
+ Object.freeze(analyzed.value.passes),
599
+ info
600
+ )
601
+ );
602
+ }
603
+ addPass(name, pass) {
604
+ const writable = this.ensureWritable();
605
+ if (!writable.ok) return writable;
606
+ if (this.passNames.has(name)) {
607
+ return err(
608
+ new RenderGraphError({
609
+ code: "duplicate-pass-name",
610
+ expected: `pass name '${name}' is unique within one builder`,
611
+ hint: `rename the second '${name}' pass; names are diagnostics, not identity`,
612
+ detail: { passName: name }
613
+ })
614
+ );
615
+ }
616
+ for (const access of pass.descriptor.accesses) {
617
+ const valid = this.validateAccessHandle(name, access);
618
+ if (!valid.ok) return valid;
619
+ }
620
+ this.passNames.add(name);
621
+ this.passes.push({ id: this.passes.length, name, pass });
622
+ return ok(void 0);
623
+ }
624
+ validateAccessHandle(passName, access) {
625
+ const data = handleData(access.resource);
626
+ if (data === void 0 || data.owner !== this.owner) {
627
+ return err(this.foreignHandleError(passName));
628
+ }
629
+ if (data.kind === "texture-view") {
630
+ if (!this.views.has(data.id)) return err(this.foreignHandleError(passName));
631
+ return ok(void 0);
632
+ }
633
+ if (data.kind !== "buffer" || !this.resources.has(data.id)) {
634
+ return err(this.foreignHandleError(passName));
635
+ }
636
+ return ok(void 0);
637
+ }
638
+ analyze(caps) {
639
+ const usageByResource = /* @__PURE__ */ new Map();
640
+ const firstUseByResource = /* @__PURE__ */ new Map();
641
+ const lastUseByResource = /* @__PURE__ */ new Map();
642
+ const compiledPasses = [];
643
+ const history = [];
644
+ for (let passIndex = 0; passIndex < this.passes.length; passIndex++) {
645
+ const pass = this.passes[passIndex];
646
+ if (pass === void 0) continue;
647
+ const capability = this.validateCapabilities(pass, caps);
648
+ if (!capability.ok) return capability;
649
+ const normalized = [];
650
+ for (const access of pass.pass.descriptor.accesses) {
651
+ const item = this.normalizeAccess(passIndex, pass, access);
652
+ if (!item.ok) return item;
653
+ normalized.push(item.value);
654
+ const usage = this.resources.get(item.value.resourceId)?.kind === "buffer" ? bufferUsage(access.usage) : textureUsage(access.usage);
655
+ usageByResource.set(
656
+ item.value.resourceId,
657
+ (usageByResource.get(item.value.resourceId) ?? 0) | usage
658
+ );
659
+ if (!firstUseByResource.has(item.value.resourceId)) {
660
+ firstUseByResource.set(item.value.resourceId, passIndex);
661
+ }
662
+ lastUseByResource.set(item.value.resourceId, passIndex);
663
+ }
664
+ const conflict = this.validatePassAccesses(pass, normalized);
665
+ if (!conflict.ok) return conflict;
666
+ const attachments = this.validateAttachments(pass);
667
+ if (!attachments.ok) return attachments;
668
+ const dependencies = /* @__PURE__ */ new Set();
669
+ for (const current of normalized) {
670
+ if (current.read) {
671
+ const priorWrite = this.findPriorWrite(history, current);
672
+ if (priorWrite !== void 0) {
673
+ dependencies.add(priorWrite.passIndex);
674
+ } else if (this.resources.get(current.resourceId)?.origin === "created") {
675
+ const label = this.resources.get(current.resourceId)?.label;
676
+ return err(
677
+ new RenderGraphError({
678
+ code: "uninitialized-read",
679
+ expected: `graph-created resource '${label}' is written before pass '${pass.name}' reads it`,
680
+ hint: "add a clear/write/copy-dst pass before the first read, or import initialized data",
681
+ detail: { passName: pass.name, resourceLabel: label, usage: current.usage }
682
+ })
683
+ );
684
+ }
685
+ }
686
+ if (current.write) {
687
+ for (const dependency of this.findWriteDependencies(history, current)) {
688
+ dependencies.add(dependency);
689
+ }
690
+ }
691
+ }
692
+ history.push(...normalized);
693
+ compiledPasses.push({
694
+ ...pass,
695
+ dependencies: Object.freeze([...dependencies].sort((left, right) => left - right)),
696
+ resourceIds: new Set(normalized.map((access) => access.resourceId)),
697
+ viewIds: new Set(
698
+ normalized.flatMap((access) => access.viewId === void 0 ? [] : [access.viewId])
699
+ )
700
+ });
701
+ }
702
+ for (const [resourceId, usage] of usageByResource) {
703
+ const resource = this.resources.get(resourceId);
704
+ if (resource?.origin !== "imported") continue;
705
+ if ((resource.descriptor.usage & usage) !== usage) {
706
+ return err(
707
+ new RenderGraphError({
708
+ code: "import-usage-mismatch",
709
+ expected: `imported resource '${resource.label}' physical usage contains derived graph usage ${usage}`,
710
+ hint: "recreate the imported resource with every usage declared by graph accesses",
711
+ detail: {
712
+ resourceLabel: resource.label,
713
+ field: "usage",
714
+ expected: String(usage),
715
+ actual: resource.descriptor.usage
716
+ }
717
+ })
718
+ );
719
+ }
720
+ }
721
+ return ok({
722
+ passes: compiledPasses,
723
+ usageByResource,
724
+ firstUseByResource,
725
+ lastUseByResource
726
+ });
727
+ }
728
+ normalizeAccess(passIndex, pass, access) {
729
+ const passName = pass.name;
730
+ const data = handleData(access.resource);
731
+ if (data === void 0 || data.owner !== this.owner) {
732
+ return err(this.foreignHandleError(passName));
733
+ }
734
+ const mode = this.accessModeForPass(pass, access);
735
+ if (data.kind === "buffer") {
736
+ return ok({ passIndex, passName, resourceId: data.id, usage: access.usage, ...mode });
737
+ }
738
+ if (data.kind !== "texture-view") return err(this.foreignHandleError(passName));
739
+ const view = this.views.get(data.id);
740
+ const texture = this.resources.get(data.textureId);
741
+ if (view === void 0 || texture?.kind !== "texture") {
742
+ return err(this.foreignHandleError(passName));
743
+ }
744
+ const mipLevels = texture.descriptor.mipLevelCount ?? 1;
745
+ const layers = typeof texture.descriptor.size === "object" ? texture.descriptor.size.depthOrArrayLayers ?? 1 : 1;
746
+ const mipStart = view.descriptor.baseMipLevel ?? 0;
747
+ const layerStart = view.descriptor.baseArrayLayer ?? 0;
748
+ return ok({
749
+ passIndex,
750
+ passName,
751
+ resourceId: data.textureId,
752
+ viewId: data.id,
753
+ usage: access.usage,
754
+ ...mode,
755
+ range: {
756
+ mipStart,
757
+ mipEnd: mipStart + (view.descriptor.mipLevelCount ?? mipLevels - mipStart),
758
+ layerStart,
759
+ layerEnd: layerStart + (view.descriptor.arrayLayerCount ?? layers - layerStart),
760
+ aspect: view.descriptor.aspect ?? "all"
761
+ }
762
+ });
763
+ }
764
+ accessModeForPass(pass, access) {
765
+ const base = accessMode(access.usage);
766
+ if (pass.pass.kind !== "raster" || handleData(access.resource)?.kind !== "texture-view") {
767
+ return base;
768
+ }
769
+ if (access.usage === "color-attachment") {
770
+ const attachment = pass.pass.descriptor.colorAttachments.find(
771
+ (candidate) => candidate.view === access.resource
772
+ );
773
+ return attachment?.loadOp === "load" ? { read: true, write: true } : base;
774
+ }
775
+ if (access.usage === "depth-stencil-write") {
776
+ const attachment = pass.pass.descriptor.depthStencilAttachment;
777
+ if (attachment?.view === access.resource && (attachment.depthLoadOp === "load" || attachment.stencilLoadOp === "load")) {
778
+ return { read: true, write: true };
779
+ }
780
+ }
781
+ return base;
782
+ }
783
+ validateCapabilities(pass, caps) {
784
+ if (pass.pass.kind === "compute" && !caps.compute) {
785
+ return this.capabilityError(pass.name, "compute");
786
+ }
787
+ for (const access of pass.pass.descriptor.accesses) {
788
+ if ((access.usage === "storage-read" || access.usage === "storage-write" || access.usage === "storage-read-write") && handleData(access.resource)?.kind === "buffer" && !caps.storageBuffer) {
789
+ return this.capabilityError(pass.name, "storage-buffer", access);
790
+ }
791
+ if ((access.usage === "storage-read" || access.usage === "storage-write" || access.usage === "storage-read-write") && handleData(access.resource)?.kind === "texture-view" && !caps.storageTexture) {
792
+ return this.capabilityError(pass.name, "storage-texture", access);
793
+ }
794
+ if (access.usage === "indirect-read" && !caps.indirectDrawing) {
795
+ return this.capabilityError(pass.name, "indirect", access);
796
+ }
797
+ }
798
+ return ok(void 0);
799
+ }
800
+ capabilityError(passName, capability, access) {
801
+ const resourceId = access === void 0 ? void 0 : accessResourceId(access);
802
+ return err(
803
+ new RenderGraphError({
804
+ code: "capability-missing",
805
+ expected: `pass '${passName}' is built only when capability '${capability}' is available`,
806
+ hint: "select the fallback algorithm before adding this pass to the builder",
807
+ detail: {
808
+ passName,
809
+ capability,
810
+ ...resourceId === void 0 ? {} : { resourceLabel: this.resources.get(resourceId)?.label, usage: access?.usage }
811
+ }
812
+ })
813
+ );
814
+ }
815
+ validatePassAccesses(pass, accesses) {
816
+ for (let leftIndex = 0; leftIndex < accesses.length; leftIndex++) {
817
+ const left = accesses[leftIndex];
818
+ if (left === void 0) continue;
819
+ for (let rightIndex = leftIndex + 1; rightIndex < accesses.length; rightIndex++) {
820
+ const right = accesses[rightIndex];
821
+ if (right === void 0 || left.resourceId !== right.resourceId || !rangesOverlap(left.range, right.range)) {
822
+ continue;
823
+ }
824
+ if (!left.write && !right.write) continue;
825
+ if (left.usage === right.usage && left.usage === "storage-read-write") continue;
826
+ const label = this.resources.get(left.resourceId)?.label;
827
+ return err(
828
+ new RenderGraphError({
829
+ code: "access-conflict",
830
+ expected: `pass '${pass.name}' uses resource '${label}' in one compatible WebGPU usage scope`,
831
+ hint: "split conflicting read/write roles into ordered passes or use storage-read-write once",
832
+ detail: {
833
+ passName: pass.name,
834
+ resourceLabel: label,
835
+ accesses: [left.usage, right.usage]
836
+ }
837
+ })
838
+ );
839
+ }
840
+ }
841
+ if (pass.pass.kind === "copy") {
842
+ const invalid = accesses.find(
843
+ (access) => access.usage !== "copy-src" && access.usage !== "copy-dst"
844
+ );
845
+ if (invalid !== void 0) {
846
+ return err(
847
+ new RenderGraphError({
848
+ code: "access-conflict",
849
+ expected: `copy pass '${pass.name}' declares only copy-src/copy-dst accesses`,
850
+ hint: "move shader or attachment work into raster/compute passes",
851
+ detail: { passName: pass.name, usage: invalid.usage }
852
+ })
853
+ );
854
+ }
855
+ }
856
+ return ok(void 0);
857
+ }
858
+ validateAttachments(pass) {
859
+ if (pass.pass.kind !== "raster") return ok(void 0);
860
+ const accesses = pass.pass.descriptor.accesses;
861
+ const has = (view, usage) => accesses.some((access) => access.resource === view && access.usage === usage);
862
+ for (const attachment of pass.pass.descriptor.colorAttachments) {
863
+ if (!has(attachment.view, "color-attachment")) {
864
+ return this.missingAttachmentAccess(pass.name, attachment.view, "color-attachment");
865
+ }
866
+ if (attachment.resolveTarget !== void 0 && !has(attachment.resolveTarget, "color-attachment")) {
867
+ return this.missingAttachmentAccess(
868
+ pass.name,
869
+ attachment.resolveTarget,
870
+ "color-attachment"
871
+ );
872
+ }
873
+ }
874
+ const depth = pass.pass.descriptor.depthStencilAttachment;
875
+ if (depth !== void 0) {
876
+ const usage = depth.depthReadOnly === true ? "depth-stencil-read" : "depth-stencil-write";
877
+ if (!has(depth.view, usage))
878
+ return this.missingAttachmentAccess(pass.name, depth.view, usage);
879
+ }
880
+ return ok(void 0);
881
+ }
882
+ missingAttachmentAccess(passName, view, usage) {
883
+ const data = handleData(view);
884
+ const label = data?.kind === "texture-view" ? this.views.get(data.id)?.label : void 0;
885
+ return err(
886
+ new RenderGraphError({
887
+ code: "resource-not-declared-by-pass",
888
+ expected: `raster pass '${passName}' attachment '${label}' declares '${usage}' access`,
889
+ hint: "add the attachment view and matching usage to accesses",
890
+ detail: { passName, resourceLabel: label, usage }
891
+ })
892
+ );
893
+ }
894
+ findPriorWrite(history, current) {
895
+ for (let index = history.length - 1; index >= 0; index--) {
896
+ const prior = history[index];
897
+ if (prior !== void 0 && prior.resourceId === current.resourceId && prior.write && rangesOverlap(prior.range, current.range)) {
898
+ return prior;
899
+ }
900
+ }
901
+ return void 0;
902
+ }
903
+ findWriteDependencies(history, current) {
904
+ const dependencies = /* @__PURE__ */ new Set();
905
+ for (let index = history.length - 1; index >= 0; index--) {
906
+ const prior = history[index];
907
+ if (prior === void 0 || prior.resourceId !== current.resourceId || !rangesOverlap(prior.range, current.range)) {
908
+ continue;
909
+ }
910
+ if (prior.read) dependencies.add(prior.passIndex);
911
+ if (prior.write) {
912
+ dependencies.add(prior.passIndex);
913
+ break;
914
+ }
915
+ }
916
+ return [...dependencies];
917
+ }
918
+ validateDescriptors(surfaceSize) {
919
+ if (surfaceSize.width <= 0 || surfaceSize.height <= 0) {
920
+ return err(
921
+ new RenderGraphError({
922
+ code: "resource-descriptor-invalid",
923
+ expected: "surfaceSize width and height are positive integers",
924
+ hint: "compile after the render surface has a non-zero physical extent",
925
+ detail: {
926
+ resourceLabel: "surface",
927
+ field: "surfaceSize",
928
+ expected: "width > 0 and height > 0",
929
+ actual: `${surfaceSize.width}x${surfaceSize.height}`
930
+ }
931
+ })
932
+ );
933
+ }
934
+ for (const resource of this.resources.values()) {
935
+ if (resource.kind === "buffer" && resource.descriptor.size <= 0) {
936
+ return err(
937
+ new RenderGraphError({
938
+ code: "resource-descriptor-invalid",
939
+ expected: `buffer '${resource.label}' size is greater than zero`,
940
+ hint: "derive a positive byte size before creating/importing the buffer",
941
+ detail: {
942
+ resourceLabel: resource.label,
943
+ field: "size",
944
+ expected: "size > 0",
945
+ actual: resource.descriptor.size
946
+ }
947
+ })
948
+ );
949
+ }
950
+ if (resource.kind === "texture") {
951
+ const extent = this.resolveExtent(resource.descriptor.size, surfaceSize);
952
+ if (extent.width <= 0 || extent.height <= 0 || extent.depthOrArrayLayers <= 0) {
953
+ return err(
954
+ new RenderGraphError({
955
+ code: "resource-descriptor-invalid",
956
+ expected: `texture '${resource.label}' extent is positive`,
957
+ hint: "repair the authored extent or compile surface size",
958
+ detail: {
959
+ resourceLabel: resource.label,
960
+ field: "size",
961
+ expected: "all extent axes > 0",
962
+ actual: `${extent.width}x${extent.height}x${extent.depthOrArrayLayers}`
963
+ }
964
+ })
965
+ );
966
+ }
967
+ }
968
+ }
969
+ return ok(void 0);
970
+ }
971
+ allocate(options, usageByResource, firstUseByResource, lastUseByResource) {
972
+ const compiledResources = /* @__PURE__ */ new Map();
973
+ const compiledViews = /* @__PURE__ */ new Map();
974
+ const createdTextures = [];
975
+ const createdBuffers = [];
976
+ const discard = () => {
977
+ for (const texture of createdTextures) options.device.destroyTexture(texture);
978
+ for (const buffer of createdBuffers) options.device.destroyBuffer(buffer);
979
+ };
980
+ for (const resource of this.resources.values()) {
981
+ const usage = usageByResource.get(resource.id) ?? 0;
982
+ let texture;
983
+ let buffer;
984
+ if (resource.origin === "created" && usage !== 0) {
985
+ if (resource.kind === "texture") {
986
+ const extent = this.resolveExtent(resource.descriptor.size, options.surfaceSize);
987
+ const created = options.device.createTexture({
988
+ label: resource.label,
989
+ size: extent,
990
+ mipLevelCount: resource.descriptor.mipLevelCount ?? 1,
991
+ sampleCount: resource.descriptor.sampleCount ?? 1,
992
+ dimension: resource.descriptor.dimension ?? "2d",
993
+ format: resource.descriptor.format,
994
+ usage,
995
+ viewFormats: [...resource.descriptor.viewFormats ?? []]
996
+ });
997
+ if (!created.ok) {
998
+ discard();
999
+ return err(
1000
+ new RenderGraphError({
1001
+ code: "resource-allocation-failed",
1002
+ expected: `RHI creates graph texture '${resource.label}'`,
1003
+ hint: "inspect detail.rhiCode and repair the descriptor/capability route",
1004
+ detail: { resourceKey: resource.label, rhiCode: created.error.code }
1005
+ })
1006
+ );
1007
+ }
1008
+ texture = created.value;
1009
+ createdTextures.push(texture);
1010
+ } else {
1011
+ const created = options.device.createBuffer({
1012
+ label: resource.label,
1013
+ size: resource.descriptor.size,
1014
+ usage,
1015
+ mappedAtCreation: resource.descriptor.mappedAtCreation ?? false
1016
+ });
1017
+ if (!created.ok) {
1018
+ discard();
1019
+ return err(
1020
+ new RenderGraphError({
1021
+ code: "resource-allocation-failed",
1022
+ expected: `RHI creates graph buffer '${resource.label}'`,
1023
+ hint: "inspect detail.rhiCode and repair the descriptor/capability route",
1024
+ detail: { resourceKey: resource.label, rhiCode: created.error.code }
1025
+ })
1026
+ );
1027
+ }
1028
+ buffer = created.value;
1029
+ createdBuffers.push(buffer);
1030
+ }
1031
+ }
1032
+ compiledResources.set(resource.id, {
1033
+ record: resource,
1034
+ usage,
1035
+ firstUse: firstUseByResource.get(resource.id) ?? null,
1036
+ lastUse: lastUseByResource.get(resource.id) ?? null,
1037
+ ...texture === void 0 ? {} : { texture },
1038
+ ...buffer === void 0 ? {} : { buffer }
1039
+ });
1040
+ }
1041
+ for (const view of this.views.values()) {
1042
+ const resource = compiledResources.get(view.textureId);
1043
+ let physicalView;
1044
+ if (resource?.record.origin === "created" && resource.texture !== void 0) {
1045
+ const created = options.device.createTextureView(resource.texture, view.descriptor);
1046
+ if (!created.ok) {
1047
+ discard();
1048
+ return err(
1049
+ new RenderGraphError({
1050
+ code: "resource-allocation-failed",
1051
+ expected: `RHI creates graph texture view '${view.label}'`,
1052
+ hint: "inspect detail.rhiCode and repair the view descriptor",
1053
+ detail: { resourceKey: view.label, rhiCode: created.error.code }
1054
+ })
1055
+ );
1056
+ }
1057
+ physicalView = created.value;
1058
+ }
1059
+ compiledViews.set(view.id, {
1060
+ record: view,
1061
+ ...physicalView === void 0 ? {} : { view: physicalView }
1062
+ });
1063
+ }
1064
+ return ok({ resources: compiledResources, views: compiledViews });
1065
+ }
1066
+ resolveExtent(extent, surface) {
1067
+ if (extent === "surface") return { ...surface, depthOrArrayLayers: 1 };
1068
+ if (extent === "half-surface") {
1069
+ return {
1070
+ width: Math.ceil(surface.width / 2),
1071
+ height: Math.ceil(surface.height / 2),
1072
+ depthOrArrayLayers: 1
1073
+ };
1074
+ }
1075
+ return {
1076
+ width: extent.width,
1077
+ height: extent.height,
1078
+ depthOrArrayLayers: extent.depthOrArrayLayers ?? 1
1079
+ };
1080
+ }
1081
+ ensureWritable() {
1082
+ return this.sealed ? err(
1083
+ new RenderGraphError({
1084
+ code: "builder-sealed",
1085
+ expected: "a RenderGraphBuilder accepts declarations only before compile()",
1086
+ hint: "create a new builder for a changed topology",
1087
+ detail: {}
1088
+ })
1089
+ ) : ok(void 0);
1090
+ }
1091
+ reserveLabel(label) {
1092
+ if (this.labels.has(label)) {
1093
+ return err(
1094
+ new RenderGraphError({
1095
+ code: "duplicate-resource-label",
1096
+ expected: `resource label '${label}' is unique within one builder`,
1097
+ hint: `rename the second '${label}' declaration`,
1098
+ detail: { resourceLabel: label }
1099
+ })
1100
+ );
1101
+ }
1102
+ this.labels.add(label);
1103
+ return ok(void 0);
1104
+ }
1105
+ foreignHandleError(passName) {
1106
+ return new RenderGraphError({
1107
+ code: "foreign-resource-handle",
1108
+ expected: "every graph resource handle belongs to this builder",
1109
+ hint: "create/import/view the resource on the same builder that declares the pass",
1110
+ detail: { ...passName === void 0 ? {} : { passName } }
1111
+ });
1112
+ }
1113
+ };
1114
+
1115
+ // src/observation.ts
1116
+ var COPY_SRC = 1;
1117
+ function observationError(code, expected, hint) {
1118
+ return err(new RenderGraphError({ code, expected, hint }));
1119
+ }
1120
+ function createCurrentFrameObservationLease(descriptor, currentFrameId) {
1121
+ if (descriptor.texture === void 0 || descriptor.texture === null) {
1122
+ return observationError(
1123
+ "observation-absent",
1124
+ "a producer-owned texture handle",
1125
+ "provide the current frame color texture before requesting an observation"
1126
+ );
1127
+ }
1128
+ if (descriptor.format !== "rgba16float") {
1129
+ return observationError(
1130
+ "observation-invalid-format",
1131
+ "current-frame observation format 'rgba16float'",
1132
+ "use the producer target format without reinterpretation"
1133
+ );
1134
+ }
1135
+ if (!Number.isInteger(descriptor.size.width) || !Number.isInteger(descriptor.size.height) || descriptor.size.width <= 0 || descriptor.size.height <= 0) {
1136
+ return observationError(
1137
+ "observation-invalid-size",
1138
+ "positive integer observation width and height",
1139
+ "capture a non-empty current-frame target"
1140
+ );
1141
+ }
1142
+ if ((descriptor.usage & COPY_SRC) === 0) {
1143
+ return observationError(
1144
+ "observation-missing-copy-src",
1145
+ "current-frame texture usage includes COPY_SRC",
1146
+ "add COPY_SRC to the producer target before requesting readback"
1147
+ );
1148
+ }
1149
+ if (descriptor.frameId !== currentFrameId) {
1150
+ return observationError(
1151
+ "observation-stale",
1152
+ `observation frame ${currentFrameId}`,
1153
+ `discard frame ${descriptor.frameId} and request the current producer target`
1154
+ );
1155
+ }
1156
+ let state = "active";
1157
+ const lifetime = {
1158
+ frameId: descriptor.frameId,
1159
+ get state() {
1160
+ return state;
1161
+ }
1162
+ };
1163
+ const lease = {
1164
+ descriptor,
1165
+ lifetime,
1166
+ get state() {
1167
+ return state;
1168
+ },
1169
+ beginReadback() {
1170
+ if (state === "retired") {
1171
+ return err(
1172
+ new RenderGraphError({
1173
+ code: "observation-retired",
1174
+ expected: "active current-frame observation lease",
1175
+ hint: "submit the eager copy before the producer retires this frame"
1176
+ })
1177
+ );
1178
+ }
1179
+ return ok({ texture: descriptor.texture, descriptor, lifetime });
1180
+ },
1181
+ retire() {
1182
+ state = "retired";
1183
+ }
1184
+ };
1185
+ return ok(lease);
1186
+ }
1187
+
1188
+ // src/pass-registry.ts
1189
+ var PassRegistry = class {
1190
+ passes = [];
1191
+ add(name, descriptor, before) {
1192
+ if (this.passes.some((pass) => pass.name === name)) {
1193
+ throw new RenderGraphError({
1194
+ code: "duplicate-pass-name",
1195
+ expected: `pass name '${name}' is unique within one graph`,
1196
+ hint: `rename the second '${name}' pass; labels are diagnostics, not identity`,
1197
+ detail: { passName: name }
1198
+ });
1199
+ }
1200
+ const entry = { name, descriptor };
1201
+ const beforeIndex = before === void 0 ? -1 : this.passes.findIndex((pass) => pass.name === before);
1202
+ if (beforeIndex < 0) this.passes.push(entry);
1203
+ else this.passes.splice(beforeIndex, 0, entry);
1204
+ return entry;
1205
+ }
1206
+ list() {
1207
+ return this.passes;
1208
+ }
1209
+ count() {
1210
+ return this.passes.length;
1211
+ }
1212
+ };
1213
+
1214
+ // src/pipeline/color-value-domain.ts
1215
+ var COLOR_VALUE_DOMAINS = ["linear-hdr", "linear-ldr", "display-encoded"];
1216
+ function isColorValueDomain(value) {
1217
+ return typeof value === "string" && COLOR_VALUE_DOMAINS.includes(value);
1218
+ }
1219
+ function invalidDomain(value) {
1220
+ return new RenderGraphError({
1221
+ code: "invalid-color-domain",
1222
+ expected: `domain is one of ${COLOR_VALUE_DOMAINS.join(", ")}`,
1223
+ hint: "set an explicit color domain; do not infer it from the attachment format",
1224
+ detail: { value: String(value) }
1225
+ });
1226
+ }
1227
+ function missingDomain(resourceKey) {
1228
+ return new RenderGraphError({
1229
+ code: "missing-color-domain",
1230
+ expected: "every connected color resource has an explicit domain",
1231
+ hint: `add domain to the color resource descriptor${resourceKey === void 0 ? "" : ` '${resourceKey}'`}`,
1232
+ detail: { resourceKey: resourceKey ?? "<descriptor>" }
1233
+ });
1234
+ }
1235
+ function serializeColorValueDomain(domain) {
1236
+ if (!isColorValueDomain(domain)) throw invalidDomain(domain);
1237
+ return JSON.stringify(domain);
1238
+ }
1239
+ function deserializeColorValueDomain(value) {
1240
+ let candidate = value;
1241
+ if (typeof value === "string") {
1242
+ try {
1243
+ candidate = JSON.parse(value);
1244
+ } catch {
1245
+ candidate = value;
1246
+ }
1247
+ }
1248
+ return isColorValueDomain(candidate) ? ok(candidate) : err(invalidDomain(candidate));
1249
+ }
1250
+ function serializeColorResourceDescriptor(descriptor) {
1251
+ if (!isColorValueDomain(descriptor.domain)) throw invalidDomain(descriptor.domain);
1252
+ return JSON.stringify(descriptor);
1253
+ }
1254
+ function deserializeColorResourceDescriptor(value) {
1255
+ if (typeof value !== "object" || value === null) return err(missingDomain());
1256
+ const candidate = value;
1257
+ if (candidate.domain === void 0) return err(missingDomain());
1258
+ if (!isColorValueDomain(candidate.domain)) return err(invalidDomain(candidate.domain));
1259
+ if (typeof candidate.format !== "string" || candidate.format.length === 0) {
1260
+ return err(
1261
+ new RenderGraphError({
1262
+ code: "invalid-color-domain",
1263
+ expected: "color resource descriptor includes a non-empty format",
1264
+ hint: "set format separately from the explicit color domain",
1265
+ detail: { value: String(candidate.format) }
1266
+ })
1267
+ );
1268
+ }
1269
+ return ok({ domain: candidate.domain, format: candidate.format });
1270
+ }
1271
+ function conversionMatches(source, destination, conversion) {
1272
+ if (conversion.kind === "encode-srgb") {
1273
+ return (source === "linear-hdr" || source === "linear-ldr") && destination === "display-encoded";
1274
+ }
1275
+ if (conversion.kind === "decode-srgb") {
1276
+ return source === "display-encoded" && (destination === "linear-hdr" || destination === "linear-ldr");
1277
+ }
1278
+ return source === "linear-hdr" && (destination === "linear-ldr" || destination === "display-encoded");
1279
+ }
1280
+ function validateColorDomainConnection(source, destination, conversion) {
1281
+ if (source === void 0 || source === null) return { ok: false, error: missingDomain("source") };
1282
+ if (destination === void 0 || destination === null) {
1283
+ return { ok: false, error: missingDomain("destination") };
1284
+ }
1285
+ if (!isColorValueDomain(source)) return { ok: false, error: invalidDomain(source) };
1286
+ if (!isColorValueDomain(destination)) return { ok: false, error: invalidDomain(destination) };
1287
+ if (source === destination) return { ok: true };
1288
+ if (conversion !== void 0 && conversionMatches(source, destination, conversion)) {
1289
+ return { ok: true };
1290
+ }
1291
+ return {
1292
+ ok: false,
1293
+ error: new RenderGraphError({
1294
+ code: "color-domain-mismatch",
1295
+ expected: `source and destination share a domain or use an explicit valid conversion (${source} -> ${destination})`,
1296
+ hint: "insert an explicit linear blend or output encoding pass; never mix into an encoded destination",
1297
+ detail: { sourceDomain: source, destinationDomain: destination }
1298
+ })
1299
+ };
1300
+ }
1301
+
1302
+ // src/resource-registry.ts
1303
+ var ResourceRegistry = class {
1304
+ resources = /* @__PURE__ */ new Map();
1305
+ add(key, descriptor) {
1306
+ const entry = {
1307
+ key,
1308
+ descriptor,
1309
+ lifetime: descriptor.lifetime
1310
+ };
1311
+ return this.register(entry);
1312
+ }
1313
+ /**
1314
+ * Register a color target resource (D-8).
1315
+ * Same semantics as addResource with kind:'texture' plus GPU texture
1316
+ * allocation metadata. Existing callers default to a transient target.
1317
+ */
1318
+ addColorTarget(name, desc) {
1319
+ const lifetime = desc.lifetime ?? "transient";
1320
+ const colorTargetMeta = {
1321
+ format: desc.format,
1322
+ size: desc.size,
1323
+ sample: desc.sample ?? 1,
1324
+ usage: desc.usage ?? 16 | 4,
1325
+ // RENDER_ATTACHMENT | TEXTURE_BINDING
1326
+ ...desc.domain !== void 0 ? { domain: desc.domain } : {},
1327
+ ...desc.viewFormats !== void 0 ? { viewFormats: desc.viewFormats } : {}
1328
+ };
1329
+ const entry = {
1330
+ key: name,
1331
+ descriptor: { kind: "texture", lifetime },
1332
+ lifetime,
1333
+ colorTarget: colorTargetMeta
1334
+ };
1335
+ return this.register(entry);
1336
+ }
1337
+ /**
1338
+ * Register a color target alias that folds into the source's physical
1339
+ * texture at compile time (KB-1 MoveNode pattern, D-2).
1340
+ * The source must already be registered via addColorTarget.
1341
+ */
1342
+ addColorTargetAlias(name, source) {
1343
+ if (this.resources.has(name)) return this.duplicateResource(name);
1344
+ const sourceMeta = this.resources.get(source)?.colorTarget;
1345
+ if (sourceMeta === void 0) {
1346
+ return err(
1347
+ new RenderGraphError({
1348
+ code: "alias-source-missing",
1349
+ expected: `alias '${name}' source '${source}' must be a registered color target`,
1350
+ hint: `call addColorTarget('${source}', ...) before retrying alias '${name}'`,
1351
+ detail: { aliasKey: name, sourceKey: source }
1352
+ })
1353
+ );
1354
+ }
1355
+ const entry = {
1356
+ key: name,
1357
+ descriptor: { kind: "texture", lifetime: "transient" },
1358
+ lifetime: "transient",
1359
+ colorTarget: {
1360
+ format: sourceMeta.format,
1361
+ size: sourceMeta.size,
1362
+ sample: sourceMeta.sample,
1363
+ usage: sourceMeta.usage,
1364
+ ...sourceMeta.domain !== void 0 ? { domain: sourceMeta.domain } : {},
1365
+ ...sourceMeta.viewFormats !== void 0 ? { viewFormats: sourceMeta.viewFormats } : {},
1366
+ aliasedFrom: source
1367
+ }
1368
+ };
1369
+ return this.register(entry);
1370
+ }
1371
+ duplicateResource(key) {
1372
+ return err(
1373
+ new RenderGraphError({
1374
+ code: "duplicate-resource",
1375
+ expected: `resource key '${key}' registered exactly once`,
1376
+ hint: `remove the duplicate resource declaration for '${key}' or use a different key`,
1377
+ detail: { resourceKey: key }
1378
+ })
1379
+ );
1380
+ }
1381
+ register(entry) {
1382
+ if (this.resources.has(entry.key)) return this.duplicateResource(entry.key);
1383
+ this.resources.set(entry.key, entry);
1384
+ return ok(entry);
1385
+ }
1386
+ get(key) {
1387
+ return this.resources.get(key);
1388
+ }
1389
+ getColorTargetMeta(key) {
1390
+ return this.resources.get(key)?.colorTarget;
1391
+ }
1392
+ has(key) {
1393
+ return this.resources.has(key);
1394
+ }
1395
+ entries() {
1396
+ return this.resources.values();
1397
+ }
1398
+ };
1399
+
1400
+ // src/graph.ts
1401
+ function poolKey(meta) {
1402
+ return `${meta.format}:${meta.width}x${meta.height}:${meta.usage}:${meta.sample}:${JSON.stringify(meta.viewFormats)}`;
1403
+ }
1404
+ var VALID_GPU_TEXTURE_FORMATS = [
1405
+ "r8unorm",
1406
+ "r8snorm",
1407
+ "r8uint",
1408
+ "r8sint",
1409
+ "r16unorm",
1410
+ "r16snorm",
1411
+ "r16uint",
1412
+ "r16sint",
1413
+ "r16float",
1414
+ "rg8unorm",
1415
+ "rg8snorm",
1416
+ "rg8uint",
1417
+ "rg8sint",
1418
+ "r32uint",
1419
+ "r32sint",
1420
+ "r32float",
1421
+ "rg16unorm",
1422
+ "rg16snorm",
1423
+ "rg16uint",
1424
+ "rg16sint",
1425
+ "rg16float",
1426
+ "rgba8unorm",
1427
+ "rgba8unorm-srgb",
1428
+ "rgba8snorm",
1429
+ "rgba8uint",
1430
+ "rgba8sint",
1431
+ "bgra8unorm",
1432
+ "bgra8unorm-srgb",
1433
+ "rgb9e5ufloat",
1434
+ "rgb10a2uint",
1435
+ "rgb10a2unorm",
1436
+ "rg11b10ufloat",
1437
+ "rg32uint",
1438
+ "rg32sint",
1439
+ "rg32float",
1440
+ "rgba16unorm",
1441
+ "rgba16snorm",
1442
+ "rgba16uint",
1443
+ "rgba16sint",
1444
+ "rgba16float",
1445
+ "rgba32uint",
1446
+ "rgba32sint",
1447
+ "rgba32float",
1448
+ "stencil8",
1449
+ "depth16unorm",
1450
+ "depth24plus",
1451
+ "depth24plus-stencil8",
1452
+ "depth32float",
1453
+ "depth32float-stencil8",
1454
+ "bc1-rgba-unorm",
1455
+ "bc1-rgba-unorm-srgb",
1456
+ "bc2-rgba-unorm",
1457
+ "bc2-rgba-unorm-srgb",
1458
+ "bc3-rgba-unorm",
1459
+ "bc3-rgba-unorm-srgb",
1460
+ "bc4-r-unorm",
1461
+ "bc4-r-snorm",
1462
+ "bc5-rg-unorm",
1463
+ "bc5-rg-snorm",
1464
+ "bc6h-rgb-ufloat",
1465
+ "bc6h-rgb-float",
1466
+ "bc7-rgba-unorm",
1467
+ "bc7-rgba-unorm-srgb",
1468
+ "etc2-rgb8unorm",
1469
+ "etc2-rgb8unorm-srgb",
1470
+ "etc2-rgb8a1unorm",
1471
+ "etc2-rgb8a1unorm-srgb",
1472
+ "etc2-rgba8unorm",
1473
+ "etc2-rgba8unorm-srgb",
1474
+ "eac-r11unorm",
1475
+ "eac-r11snorm",
1476
+ "eac-rg11unorm",
1477
+ "eac-rg11snorm",
1478
+ "astc-4x4-unorm",
1479
+ "astc-4x4-unorm-srgb",
1480
+ "astc-5x4-unorm",
1481
+ "astc-5x4-unorm-srgb",
1482
+ "astc-5x5-unorm",
1483
+ "astc-5x5-unorm-srgb",
1484
+ "astc-6x5-unorm",
1485
+ "astc-6x5-unorm-srgb",
1486
+ "astc-6x6-unorm",
1487
+ "astc-6x6-unorm-srgb",
1488
+ "astc-8x5-unorm",
1489
+ "astc-8x5-unorm-srgb",
1490
+ "astc-8x6-unorm",
1491
+ "astc-8x6-unorm-srgb",
1492
+ "astc-8x8-unorm",
1493
+ "astc-8x8-unorm-srgb",
1494
+ "astc-10x5-unorm",
1495
+ "astc-10x5-unorm-srgb",
1496
+ "astc-10x6-unorm",
1497
+ "astc-10x6-unorm-srgb",
1498
+ "astc-10x8-unorm",
1499
+ "astc-10x8-unorm-srgb",
1500
+ "astc-10x10-unorm",
1501
+ "astc-10x10-unorm-srgb",
1502
+ "astc-12x10-unorm",
1503
+ "astc-12x10-unorm-srgb",
1504
+ "astc-12x12-unorm",
1505
+ "astc-12x12-unorm-srgb"
1506
+ ];
1507
+ var VALID_GPU_TEXTURE_FORMAT_SET = new Set(VALID_GPU_TEXTURE_FORMATS);
1508
+ var RenderGraph = class {
1509
+ resources = new ResourceRegistry();
1510
+ passes = new PassRegistry();
1511
+ compiled = null;
1512
+ /** Transient texture pool: keyed by descriptor, reused across compiles (D-2). */
1513
+ transientPool = /* @__PURE__ */ new Map();
1514
+ /**
1515
+ * Pending-destroy queue (bug-20260622): replaced textures awaiting GPU
1516
+ * retirement before actual device.destroyTexture. drainTransient(),
1517
+ * setTransientEntry(), and setPersistentEntry() push here instead of destroying immediately;
1518
+ * reclaimRetiredTransients() (called post-queue.submit in recordFrame) drains
1519
+ * the queue when the GPU signals onSubmittedWorkDone.
1520
+ */
1521
+ pendingDestroy = [];
1522
+ /** Persistent textures: keyed by resource name, kept across compiles. */
1523
+ persistentTextures = /* @__PURE__ */ new Map();
1524
+ /** Swap-chain size for resolving 'swapchain' / 'half-swapchain' sizes. */
1525
+ swapChainWidth = 800;
1526
+ swapChainHeight = 600;
1527
+ /** Last compile-time swap-chain size; diff triggers recompile-invalidation. */
1528
+ compiledWidth = 800;
1529
+ compiledHeight = 600;
1530
+ /**
1531
+ * feat-20260612 M-4 / w15: device reference stashed at compile-time so
1532
+ * drain() can release pooled textures via device.destroyTexture without
1533
+ * a separate parameter. Set by compile(); null until the first compile.
1534
+ * Render-graph stays RHI-pure (no runtime dep): the destroy bookkeeping
1535
+ * SSOT is the RHI shim, exactly as GpuTexture.destroy() routes through it.
1536
+ */
1537
+ lastDevice = null;
1538
+ /**
1539
+ * Set the current swap-chain dimensions (w7).
1540
+ * The compile allocation phase uses this to resolve 'swapchain' and
1541
+ * 'half-swapchain' size specifiers. Returns true when dimensions differ
1542
+ * from the last compile, signalling that a recompile is needed.
1543
+ */
1544
+ setSwapChainSize(width, height) {
1545
+ this.swapChainWidth = width;
1546
+ this.swapChainHeight = height;
1547
+ if (width !== this.compiledWidth || height !== this.compiledHeight) {
1548
+ return true;
1549
+ }
1550
+ return false;
1551
+ }
1552
+ /**
1553
+ * w7: resolve a color-target name to its compiled TextureView.
1554
+ * Returns the GPU view after compile, or undefined if not yet compiled
1555
+ * or the name was not registered via addColorTarget.
1556
+ */
1557
+ getColorTargetView(name) {
1558
+ return this.compiled?.resolvedTextures.get(name);
1559
+ }
1560
+ /**
1561
+ * w7: resolve a color-target name to its compiled GPU Texture handle.
1562
+ * Returns the texture after compile, or undefined if not yet compiled.
1563
+ */
1564
+ getColorTargetTexture(name) {
1565
+ return this.compiled?.resolvedTextures.get(`${name}::tex`);
1566
+ }
1567
+ getColorTargetDescriptor(name) {
1568
+ const meta = this.resources.getColorTargetMeta(name);
1569
+ const texture = this.compiled?.resolvedTextures.get(`${name}::tex`);
1570
+ if (meta === void 0 || texture === void 0) return void 0;
1571
+ return {
1572
+ texture,
1573
+ format: meta.format,
1574
+ size: {
1575
+ width: this.resolveWidth(meta.size),
1576
+ height: this.resolveHeight(meta.size)
1577
+ },
1578
+ usage: meta.usage,
1579
+ sample: meta.sample
1580
+ };
1581
+ }
1582
+ /**
1583
+ * Declare a color target alias: both names share the same physical texture.
1584
+ * The source must already be registered via addColorTarget.
1585
+ * Used for hdrComposited -> hdrColor folding (KB-1 / D-2). The returned
1586
+ * Result contains the opaque alias handle or a duplicate-resource error.
1587
+ */
1588
+ addColorTargetAlias(name, source) {
1589
+ const result = this.resources.addColorTargetAlias(name, source);
1590
+ if (!result.ok) return result;
1591
+ return ok(name);
1592
+ }
1593
+ addResource(key, descriptor) {
1594
+ return this.resources.add(key, descriptor);
1595
+ }
1596
+ /**
1597
+ * Declare a color target resource that the compiler will allocate as a
1598
+ * transient or persistent GPU texture (D-1 / D-8). A successful Result
1599
+ * contains an opaque string handle that can be referenced in pass read/write
1600
+ * arrays and resolved to a TextureView via resolve(name) inside a pass
1601
+ * execute closure. A duplicate key is rejected before registry publication.
1602
+ *
1603
+ * Omitted lifetime preserves the default `transient`; `persistent` retains
1604
+ * identity across unchanged compiles and replaces on descriptor drift.
1605
+ * format/size/sample/usage are stored on the resource entry for the compile
1606
+ * allocation phase (w6).
1607
+ */
1608
+ addColorTarget(name, desc) {
1609
+ const result = this.resources.addColorTarget(name, desc);
1610
+ if (!result.ok) return result;
1611
+ return ok(name);
1612
+ }
1613
+ addPass(name, descriptor) {
1614
+ return this.passes.add(name, descriptor);
1615
+ }
1616
+ /** @internal Renderer composition seam for declaring feature work at its semantic target. */
1617
+ _addPassBefore(name, before, descriptor) {
1618
+ return this.passes.add(name, descriptor, before);
1619
+ }
1620
+ addComputePass(name, descriptor) {
1621
+ return this.addComputePassAt(name, descriptor);
1622
+ }
1623
+ /** @internal Renderer composition seam for declaring feature work at its semantic target. */
1624
+ _addComputePassBefore(name, before, descriptor) {
1625
+ return this.addComputePassAt(name, descriptor, before);
1626
+ }
1627
+ addComputePassAt(name, descriptor, before) {
1628
+ return this.passes.add(
1629
+ name,
1630
+ {
1631
+ reads: descriptor.reads,
1632
+ writes: descriptor.writes,
1633
+ compute: true,
1634
+ storageBuffer: descriptor.storageBuffer ?? true,
1635
+ execute: (frame, resources) => {
1636
+ const encoder = frame.encoder;
1637
+ const begin = descriptor.begin?.(frame);
1638
+ let pass;
1639
+ try {
1640
+ pass = encoder.beginComputePass({
1641
+ label: name,
1642
+ ...begin?.timestampWrites === void 0 ? {} : { timestampWrites: begin.timestampWrites }
1643
+ });
1644
+ } catch (cause) {
1645
+ descriptor.onBeginError?.(frame, cause);
1646
+ return;
1647
+ }
1648
+ try {
1649
+ descriptor.encode({ pass, frame, resources });
1650
+ } finally {
1651
+ pass.end();
1652
+ }
1653
+ descriptor.after?.(frame);
1654
+ }
1655
+ },
1656
+ before
1657
+ );
1658
+ }
1659
+ /**
1660
+ * Validate a producer-scoped current-frame texture without exposing graph
1661
+ * resource names through the observation lease.
1662
+ */
1663
+ createCurrentFrameObservationLease(descriptor, currentFrameId) {
1664
+ return createCurrentFrameObservationLease(descriptor, currentFrameId);
1665
+ }
1666
+ /**
1667
+ * Compile the graph into an internalized form.
1668
+ *
1669
+ * Phases (plan-strategy 3.1):
1670
+ * 1. Cap-gate fail-fast
1671
+ * 2. Unknown-resource fail-fast (every pass read/write key is registered)
1672
+ * 3. Dangling-read fail-fast
1673
+ * 4. Preserve declaration order as the temporal authority
1674
+ * 5. Buffer-role resolution (AC-09 / D-6.1)
1675
+ * 6. GPU allocation for color targets (D-1) — when device is provided and the
1676
+ * graph has addColorTarget resources, allocate textures via
1677
+ * device.createTexture/createTextureView. Errors surface as
1678
+ * 'resource-alloc-failed' or 'invalid-format'.
1679
+ */
1680
+ compile(opts) {
1681
+ const passList = this.passes.list();
1682
+ const { caps, device } = opts;
1683
+ const capErr = this.validateCaps(passList, caps);
1684
+ if (capErr) return capErr;
1685
+ const colorDomainErr = this.validateColorDomains(passList);
1686
+ if (colorDomainErr) return colorDomainErr;
1687
+ const unknownErr = this.validateNoUnknownResource(passList);
1688
+ if (unknownErr) return unknownErr;
1689
+ const danglingErr = this.validateNoDanglingRead(passList);
1690
+ if (danglingErr) return danglingErr;
1691
+ const formatErr = this.validateColorTargetFormats();
1692
+ if (!formatErr.ok) return formatErr;
1693
+ const internalizedPasses = passList.map((pass) => {
1694
+ return {
1695
+ name: pass.name,
1696
+ reads: pass.descriptor.reads,
1697
+ writes: pass.descriptor.writes
1698
+ };
1699
+ });
1700
+ const resolvedBuffers = this.resolveBuffers(caps);
1701
+ const resizeDetected = this.swapChainWidth !== this.compiledWidth || this.swapChainHeight !== this.compiledHeight;
1702
+ const allocatedTextures = this.allocateColorTargets(device, resizeDetected);
1703
+ if (!allocatedTextures.ok) return allocatedTextures;
1704
+ if (resizeDetected) {
1705
+ this.drainTransient();
1706
+ }
1707
+ for (const [key, pooled] of allocatedTextures.value.transient) {
1708
+ this.setTransientEntry(key, pooled);
1709
+ }
1710
+ for (const [key, pooled] of allocatedTextures.value.persistent) {
1711
+ this.setPersistentEntry(key, pooled);
1712
+ }
1713
+ this.compiled = {
1714
+ passes: internalizedPasses,
1715
+ resolvedBuffers,
1716
+ resolvedTextures: allocatedTextures.value.resolvedTextures
1717
+ };
1718
+ this.compiledWidth = this.swapChainWidth;
1719
+ this.compiledHeight = this.swapChainHeight;
1720
+ if (device !== void 0) {
1721
+ this.lastDevice = device;
1722
+ }
1723
+ return ok(this.compiled);
1724
+ }
1725
+ /**
1726
+ * feat-20260612 M-4 / w15: release every pooled GPU texture and clear
1727
+ * the pools.
1728
+ *
1729
+ * Walks `transientPool` + `persistentTextures`, forwarding each
1730
+ * `PooledTexture.texture` opaque handle to `device.destroyTexture(...)`,
1731
+ * then clears both Maps. The destroy bookkeeping SSOT is the RHI shim
1732
+ * (architecture-principles §1 SSOT: same path GpuTexture.destroy()
1733
+ * uses); render-graph stays RHI-pure (no runtime dep).
1734
+ *
1735
+ * Plan-strategy D-7: drain covers the dispose exit path (`Renderer.dispose()`);
1736
+ * descriptor-drift replacement during compile is fenced through
1737
+ * `pendingDestroy` and `reclaimRetiredTransients()`.
1738
+ *
1739
+ * Idempotent (architecture-principles §6): a second drain on cleared
1740
+ * Maps is a no-op. drain() before any compile is also a safe no-op.
1741
+ * Per-handle errors from the RHI shim (e.g. 'destroy-after-destroy'
1742
+ * on a stale handle) are tolerated so the dispose chain can make
1743
+ * progress (mirrors gpuStore.destroyAll's swallow-and-continue
1744
+ * policy; plan-strategy D-3 / D-8). The structured error stays
1745
+ * available on the device handle for future inspector hooks.
1746
+ */
1747
+ drain() {
1748
+ const device = this.lastDevice;
1749
+ if (device === null) {
1750
+ this.transientPool.clear();
1751
+ this.persistentTextures.clear();
1752
+ this.pendingDestroy.length = 0;
1753
+ return;
1754
+ }
1755
+ for (const pooled of this.transientPool.values()) {
1756
+ try {
1757
+ device.destroyTexture(pooled.texture);
1758
+ } catch {
1759
+ }
1760
+ }
1761
+ this.transientPool.clear();
1762
+ for (const pooled of this.pendingDestroy) {
1763
+ try {
1764
+ device.destroyTexture(pooled.texture);
1765
+ } catch {
1766
+ }
1767
+ }
1768
+ this.pendingDestroy.length = 0;
1769
+ for (const pooled of this.persistentTextures.values()) {
1770
+ try {
1771
+ device.destroyTexture(pooled.texture);
1772
+ } catch {
1773
+ }
1774
+ }
1775
+ this.persistentTextures.clear();
1776
+ }
1777
+ /**
1778
+ * Relinquish every texture owned by this graph after its last frame has
1779
+ * been submitted. Unlike {@link drain}, this does not synchronously destroy
1780
+ * GPU resources: they join `pendingDestroy` and are released by
1781
+ * `reclaimRetiredTransients()` only after `onSubmittedWorkDone` resolves.
1782
+ *
1783
+ * A retired graph is no longer executable. Runtime replaces a memoized
1784
+ * per-frame graph through this entry when topology changes (rather than
1785
+ * dropping the graph and its pools), while `drain()` remains the teardown
1786
+ * path where immediate destruction is safe.
1787
+ */
1788
+ retire() {
1789
+ const device = this.lastDevice;
1790
+ if (device === null) {
1791
+ this.transientPool.clear();
1792
+ this.persistentTextures.clear();
1793
+ this.pendingDestroy.length = 0;
1794
+ this.compiled = null;
1795
+ return;
1796
+ }
1797
+ for (const pooled of this.transientPool.values()) {
1798
+ this.pendingDestroy.push(pooled);
1799
+ }
1800
+ this.transientPool.clear();
1801
+ for (const pooled of this.persistentTextures.values()) {
1802
+ this.pendingDestroy.push(pooled);
1803
+ }
1804
+ this.persistentTextures.clear();
1805
+ this.compiled = null;
1806
+ }
1807
+ /**
1808
+ * Release every transient-pool texture while keeping persistentTextures
1809
+ * intact (AC-09: resize drain, plan-strategy D-4).
1810
+ *
1811
+ * Walks `transientPool` values and forwards each `PooledTexture.texture`
1812
+ * opaque handle to `device.destroyTexture(...)`, then clears the transient
1813
+ * pool. Mirror of `drain()` but scoped to the transient pool only.
1814
+ *
1815
+ * Persistent textures survive `drainTransient` — they are only released by
1816
+ * the full `drain()` on teardown. `drainTransient` is an internal helper
1817
+ * called by `compile()` when swap-chain size changes; it is NOT a public API
1818
+ * (callers should use `drain()` for teardown).
1819
+ *
1820
+ * Idempotent (architecture-principles §6): a second drainTransient on an
1821
+ * already-cleared transient pool is a no-op.
1822
+ */
1823
+ drainTransient() {
1824
+ const device = this.lastDevice;
1825
+ if (device === null) {
1826
+ this.transientPool.clear();
1827
+ return;
1828
+ }
1829
+ for (const pooled of this.transientPool.values()) {
1830
+ this.pendingDestroy.push(pooled);
1831
+ }
1832
+ this.transientPool.clear();
1833
+ }
1834
+ /**
1835
+ * Guarded transient pool insert (AC-08, plan-strategy D-4).
1836
+ *
1837
+ * Before overwriting a key in the transient pool, destroys the old pooled
1838
+ * texture via `device.destroyTexture(...)` to prevent stranded GPU textures.
1839
+ * The guard is defensive: in current production code flow this code path is
1840
+ * unreachable (set() only follows a get() miss inside allocateColorTargets),
1841
+ * but the single-line guard costs almost nothing and closes the symmetry gap
1842
+ * (every GPU resource allocation has a paired destroy).
1843
+ *
1844
+ * When `lastDevice` is null (no device ever stashed), the old entry is
1845
+ * silently dropped without destroy (mirrors drainTransient's null-device
1846
+ * fast path).
1847
+ */
1848
+ setTransientEntry(key, pooled) {
1849
+ const old = this.transientPool.get(key);
1850
+ if (old) {
1851
+ this.pendingDestroy.push(old);
1852
+ }
1853
+ this.transientPool.set(key, pooled);
1854
+ }
1855
+ /**
1856
+ * Publish a persistent replacement only after a complete allocation succeeds.
1857
+ * The old handle remains fenced until the GPU retires work that may still
1858
+ * reference it, just like a transient replacement.
1859
+ */
1860
+ setPersistentEntry(key, pooled) {
1861
+ const old = this.persistentTextures.get(key);
1862
+ if (old && old.texture !== pooled.texture) {
1863
+ this.pendingDestroy.push(old);
1864
+ }
1865
+ this.persistentTextures.set(key, pooled);
1866
+ }
1867
+ /**
1868
+ * bug-20260622 D-2: reclaim pool textures queued in pendingDestroy after
1869
+ * the GPU has retired all prior command buffers.
1870
+ *
1871
+ * Takes a snapshot of pendingDestroy, then calls
1872
+ * `lastDevice.queue.onSubmittedWorkDone()`. When the promise resolves,
1873
+ * the snapshot items are actually destroyed via
1874
+ * `device.destroyTexture(...)` and removed from the queue.
1875
+ *
1876
+ * Idempotent (architecture-principles D-4): a second reclaim on an
1877
+ * already-drained pendingDestroy is a no-op. When lastDevice is null
1878
+ * (no device ever stashed), pendingDestroy is cleared directly.
1879
+ *
1880
+ * Per-handle destroy errors are tolerated (swallow-and-continue,
1881
+ * plan-strategy D-5) — a stale-handle destroy-after-destroy does not
1882
+ * interrupt the reclaim chain.
1883
+ */
1884
+ async reclaimRetiredTransients() {
1885
+ const device = this.lastDevice;
1886
+ if (device === null) {
1887
+ this.pendingDestroy.length = 0;
1888
+ return;
1889
+ }
1890
+ if (this.pendingDestroy.length === 0) return;
1891
+ const snapshot = this.pendingDestroy.splice(0);
1892
+ await device.queue.onSubmittedWorkDone();
1893
+ for (const pooled of snapshot) {
1894
+ try {
1895
+ device.destroyTexture(pooled.texture);
1896
+ } catch {
1897
+ }
1898
+ }
1899
+ }
1900
+ /**
1901
+ * Drop the pendingDestroy queue WITHOUT calling device.destroyTexture
1902
+ * (feat-20260622-s5 M3 / B-2 / B-AC-02).
1903
+ *
1904
+ * Used on the device-lost recover() rebuild path: the queue holds
1905
+ * PooledTexture handles minted against the now-lost device, so calling
1906
+ * destroyTexture on them against the freshly-rebuilt device is meaningless
1907
+ * (the old GPUDevice owns them; spec retires its resources implicitly when
1908
+ * it is lost). recover() calls this after `gpuStore.destroyAll()` and before
1909
+ * `tryCreateWebGPURenderer` so no stale handle reaches the new device.
1910
+ *
1911
+ * device-lost is an upstream judgement (createRenderer's health state); the
1912
+ * graph stays RHI-pure and takes no device parameter — it only exposes the
1913
+ * clear entry. Same effect as the existing null-device fast paths in drain()
1914
+ * / reclaimRetiredTransients() (`pendingDestroy.length = 0`), surfaced as a
1915
+ * method recover() can call directly. Idempotent: a second call on an
1916
+ * already-empty queue is a no-op.
1917
+ */
1918
+ clearPendingDestroy() {
1919
+ this.pendingDestroy.length = 0;
1920
+ }
1921
+ /**
1922
+ * Execute the compiled graph in declaration order, calling
1923
+ * each pass's execute closure with the provided context. Passes without an
1924
+ * execute closure are silently skipped.
1925
+ */
1926
+ execute(ctx, runPass) {
1927
+ const compiled = this.compiled;
1928
+ if (!compiled) return;
1929
+ const resolvedTextures = compiled.resolvedTextures;
1930
+ const resolveCtx = {
1931
+ resolve: (name) => resolvedTextures.get(name)
1932
+ };
1933
+ const passList = this.passes.list();
1934
+ const passByName = new Map(passList.map((p) => [p.name, p]));
1935
+ for (const internalPass of compiled.passes) {
1936
+ const entry = passByName.get(internalPass.name);
1937
+ const execute = entry?.descriptor.execute;
1938
+ if (execute) {
1939
+ if (runPass === void 0) {
1940
+ execute(ctx, resolveCtx);
1941
+ } else {
1942
+ runPass(
1943
+ internalPass.name,
1944
+ () => execute(ctx, resolveCtx)
1945
+ );
1946
+ }
1947
+ }
1948
+ }
1949
+ }
1950
+ listPasses() {
1951
+ return this.passes.list().map((p) => ({
1952
+ name: p.name,
1953
+ reads: p.descriptor.reads,
1954
+ writes: p.descriptor.writes
1955
+ }));
1956
+ }
1957
+ listResources() {
1958
+ const result = [];
1959
+ for (const entry of this.resources.entries()) {
1960
+ result.push({
1961
+ key: entry.key,
1962
+ kind: entry.descriptor.kind,
1963
+ lifetime: entry.descriptor.lifetime
1964
+ });
1965
+ }
1966
+ return result;
1967
+ }
1968
+ // ── Private helpers ────────────────────────────────────────────
1969
+ validateCaps(passList, caps) {
1970
+ for (const pass of passList) {
1971
+ const { name, descriptor } = pass;
1972
+ if (descriptor.compute && !caps.compute) {
1973
+ return err(
1974
+ new RenderGraphError({
1975
+ code: "cap-missing",
1976
+ expected: `pass '${name}' is a compute pass but caps.compute is false`,
1977
+ hint: "use a render pass path or enable compute on the backend",
1978
+ detail: { cap: "compute", passName: name }
1979
+ })
1980
+ );
1981
+ }
1982
+ if (descriptor.storageBuffer && !caps.storageBuffer) {
1983
+ return err(
1984
+ new RenderGraphError({
1985
+ code: "cap-missing",
1986
+ expected: `pass '${name}' requires storage buffer but caps.storageBuffer is false`,
1987
+ hint: "switch to uniform buffer or enable storageBuffer on the backend",
1988
+ detail: {
1989
+ cap: "storageBuffer",
1990
+ passName: name
1991
+ }
1992
+ })
1993
+ );
1994
+ }
1995
+ }
1996
+ return null;
1997
+ }
1998
+ validateColorDomains(passList) {
1999
+ for (const pass of passList) {
2000
+ for (const connection of pass.descriptor.colorConnections ?? []) {
2001
+ const source = this.resources.get(connection.source)?.colorTarget?.domain;
2002
+ const destination = this.resources.get(connection.destination)?.colorTarget?.domain;
2003
+ const validation = validateColorDomainConnection(
2004
+ source,
2005
+ destination,
2006
+ connection.conversion
2007
+ );
2008
+ if (!validation.ok) return err(validation.error);
2009
+ }
2010
+ }
2011
+ return null;
2012
+ }
2013
+ validateNoUnknownResource(passList) {
2014
+ for (const pass of passList) {
2015
+ for (const key of [...pass.descriptor.reads, ...pass.descriptor.writes]) {
2016
+ if (key === "swapchain") continue;
2017
+ if (!this.resources.has(key)) {
2018
+ return err(
2019
+ new RenderGraphError({
2020
+ code: "unknown-resource",
2021
+ expected: `pass '${pass.name}' references resource key '${key}' but it is not registered`,
2022
+ hint: `call addResource('${key}', ...) before compile, or remove '${key}' from pass '${pass.name}'`,
2023
+ detail: {
2024
+ resourceKey: key,
2025
+ passName: pass.name
2026
+ }
2027
+ })
2028
+ );
2029
+ }
2030
+ }
2031
+ }
2032
+ return null;
2033
+ }
2034
+ /**
2035
+ * Resolve every registered `kind:'buffer'` resource to a concrete RHI
2036
+ * binding type (AC-09 / D-6.1). `bufferRole='auto-storage-or-uniform'`
2037
+ * (the default when unset) picks `'read-only-storage'` when the backend
2038
+ * advertises `caps.storageBuffer`, else falls back to `'uniform'`;
2039
+ * `bufferRole='uniform'` is always `'uniform'`. Mirrors the runtime
2040
+ * `pbr-pipeline.ts` cap switch (research Finding 7), expressed here in the
2041
+ * RHI-pure graph layer so consumers never duplicate the branch.
2042
+ */
2043
+ resolveBuffers(caps) {
2044
+ const resolved = [];
2045
+ for (const entry of this.resources.entries()) {
2046
+ if (entry.descriptor.kind !== "buffer") continue;
2047
+ const role = entry.descriptor.bufferRole ?? "auto-storage-or-uniform";
2048
+ const resolvedBufferType = role === "uniform" ? "uniform" : caps.storageBuffer ? "read-only-storage" : "uniform";
2049
+ resolved.push({ key: entry.key, resolvedBufferType });
2050
+ }
2051
+ return resolved;
2052
+ }
2053
+ validateColorTargetFormats() {
2054
+ for (const entry of this.resources.entries()) {
2055
+ const meta = entry.colorTarget;
2056
+ if (!meta || VALID_GPU_TEXTURE_FORMAT_SET.has(meta.format)) continue;
2057
+ return err(
2058
+ new RenderGraphError({
2059
+ code: "invalid-format",
2060
+ expected: `addColorTarget format must be a valid GPU texture format; received '${meta.format}'`,
2061
+ hint: `replace '${meta.format}' with one of detail.expected before recompiling`,
2062
+ detail: {
2063
+ resourceKey: entry.key,
2064
+ format: meta.format,
2065
+ expected: VALID_GPU_TEXTURE_FORMATS
2066
+ }
2067
+ })
2068
+ );
2069
+ }
2070
+ return ok(void 0);
2071
+ }
2072
+ /**
2073
+ * Phase 7: allocate GPU textures for registered color targets (D-1 / D-2).
2074
+ *
2075
+ * For each addColorTarget resource, resolves the concrete size from the
2076
+ * ColorTargetSize descriptor and swapChainSize, then looks up the transient
2077
+ * pool by descriptor key. Pool hit reuses the same physical texture/view;
2078
+ * pool miss (drift) triggers device.createTexture/createTextureView rebuild.
2079
+ *
2080
+ * Alias targets (addColorTargetAlias) fold into the source's physical texture
2081
+ * (KB-1 MoveNode pattern). Persistent targets are retained across compiles
2082
+ * with size-drift rebuild.
2083
+ *
2084
+ * device === undefined is a no-op (returns an empty map).
2085
+ * Allocation is transactional: newly created textures are destroyed on any
2086
+ * failure, and pool mutations are committed only after every target succeeds.
2087
+ */
2088
+ allocateColorTargets(device, invalidateTransientPool = false) {
2089
+ const result = /* @__PURE__ */ new Map();
2090
+ if (!device || typeof device.createTexture !== "function")
2091
+ return ok({ resolvedTextures: result, transient: /* @__PURE__ */ new Map(), persistent: /* @__PURE__ */ new Map() });
2092
+ const stagedTransient = /* @__PURE__ */ new Map();
2093
+ const stagedPersistent = /* @__PURE__ */ new Map();
2094
+ const stagedAllocations = [];
2095
+ const discardStaged = () => {
2096
+ for (const pooled of stagedAllocations) {
2097
+ try {
2098
+ device.destroyTexture(pooled.texture);
2099
+ } catch {
2100
+ }
2101
+ }
2102
+ };
2103
+ for (const entry of this.resources.entries()) {
2104
+ const meta = entry.colorTarget;
2105
+ if (!meta) continue;
2106
+ if (meta.aliasedFrom !== void 0) {
2107
+ const sourceView = result.get(meta.aliasedFrom);
2108
+ const sourceTexture = result.get(`${meta.aliasedFrom}::tex`);
2109
+ if (sourceView === void 0 || sourceTexture === void 0) {
2110
+ discardStaged();
2111
+ return err(
2112
+ new RenderGraphError({
2113
+ code: "alias-source-missing",
2114
+ expected: `alias '${entry.key}' source '${meta.aliasedFrom}' must resolve to a compiled color target`,
2115
+ hint: `register color target '${meta.aliasedFrom}' before compiling alias '${entry.key}'`,
2116
+ detail: {
2117
+ aliasKey: entry.key,
2118
+ sourceKey: meta.aliasedFrom
2119
+ }
2120
+ })
2121
+ );
2122
+ }
2123
+ result.set(entry.key, sourceView);
2124
+ result.set(`${entry.key}::tex`, sourceTexture);
2125
+ continue;
2126
+ }
2127
+ const width = this.resolveWidth(meta.size);
2128
+ const height = this.resolveHeight(meta.size);
2129
+ const lifetime = entry.lifetime;
2130
+ const descriptorKey = poolKey({
2131
+ format: meta.format,
2132
+ width,
2133
+ height,
2134
+ usage: meta.usage,
2135
+ sample: meta.sample,
2136
+ viewFormats: meta.viewFormats ?? []
2137
+ });
2138
+ const key = `${entry.key}:${descriptorKey}`;
2139
+ if (lifetime === "transient") {
2140
+ const pooled2 = invalidateTransientPool ? void 0 : this.transientPool.get(key);
2141
+ if (pooled2) {
2142
+ result.set(entry.key, pooled2.view);
2143
+ result.set(`${entry.key}::tex`, pooled2.texture);
2144
+ continue;
2145
+ }
2146
+ } else if (lifetime === "persistent") {
2147
+ const persisted = this.persistentTextures.get(entry.key);
2148
+ if (persisted?.descriptorKey === descriptorKey) {
2149
+ result.set(entry.key, persisted.view);
2150
+ result.set(`${entry.key}::tex`, persisted.texture);
2151
+ continue;
2152
+ }
2153
+ }
2154
+ const texResult = device.createTexture({
2155
+ label: entry.key,
2156
+ size: { width, height, depthOrArrayLayers: 1 },
2157
+ mipLevelCount: 1,
2158
+ sampleCount: meta.sample,
2159
+ dimension: "2d",
2160
+ format: meta.format,
2161
+ usage: meta.usage,
2162
+ viewFormats: meta.viewFormats ?? []
2163
+ });
2164
+ if (!texResult.ok) {
2165
+ discardStaged();
2166
+ return err(
2167
+ new RenderGraphError({
2168
+ code: "resource-alloc-failed",
2169
+ expected: `device.createTexture must succeed for color target '${entry.key}'`,
2170
+ hint: `retry after recovering the RHI allocation failure for '${entry.key}'`,
2171
+ detail: {
2172
+ resourceKey: entry.key,
2173
+ rhiCode: texResult.error.code
2174
+ }
2175
+ })
2176
+ );
2177
+ }
2178
+ const viewResult = device.createTextureView(texResult.value, {});
2179
+ if (!viewResult.ok) {
2180
+ try {
2181
+ device.destroyTexture(texResult.value);
2182
+ } catch {
2183
+ }
2184
+ discardStaged();
2185
+ return err(
2186
+ new RenderGraphError({
2187
+ code: "resource-alloc-failed",
2188
+ expected: `device.createTextureView must succeed for color target '${entry.key}'`,
2189
+ hint: `retry after recovering the RHI view allocation failure for '${entry.key}'`,
2190
+ detail: {
2191
+ resourceKey: entry.key,
2192
+ rhiCode: viewResult.error.code
2193
+ }
2194
+ })
2195
+ );
2196
+ }
2197
+ const pooled = {
2198
+ texture: texResult.value,
2199
+ view: viewResult.value,
2200
+ descriptorKey
2201
+ };
2202
+ stagedAllocations.push(pooled);
2203
+ if (lifetime === "transient") {
2204
+ stagedTransient.set(key, pooled);
2205
+ } else {
2206
+ stagedPersistent.set(entry.key, pooled);
2207
+ }
2208
+ result.set(entry.key, viewResult.value);
2209
+ result.set(`${entry.key}::tex`, texResult.value);
2210
+ }
2211
+ return ok({
2212
+ resolvedTextures: result,
2213
+ transient: stagedTransient,
2214
+ persistent: stagedPersistent
2215
+ });
2216
+ }
2217
+ resolveWidth(size) {
2218
+ if (typeof size === "string") {
2219
+ return size === "half-swapchain" ? Math.ceil(this.swapChainWidth / 2) : this.swapChainWidth;
2220
+ }
2221
+ return size.w;
2222
+ }
2223
+ resolveHeight(size) {
2224
+ if (typeof size === "string") {
2225
+ return size === "half-swapchain" ? Math.ceil(this.swapChainHeight / 2) : this.swapChainHeight;
2226
+ }
2227
+ return size.h;
2228
+ }
2229
+ validateNoDanglingRead(passList) {
2230
+ const writers = /* @__PURE__ */ new Set();
2231
+ for (const pass of passList) {
2232
+ for (const key of pass.descriptor.reads) {
2233
+ const imported = this.resources.get(key)?.descriptor.lifetime === "persistent";
2234
+ if (key !== "swapchain" && !imported && !writers.has(key)) {
2235
+ return err(
2236
+ new RenderGraphError({
2237
+ code: "dangling-read",
2238
+ expected: `pass '${pass.name}' reads key '${key}' but no pass writes it`,
2239
+ hint: `add a pass that writes '${key}', or remove '${key}' from pass '${pass.name}' reads`,
2240
+ detail: {
2241
+ resourceKey: key,
2242
+ passName: pass.name
2243
+ }
2244
+ })
2245
+ );
2246
+ }
2247
+ }
2248
+ for (const key of pass.descriptor.writes) writers.add(key);
2249
+ }
2250
+ return null;
2251
+ }
2252
+ };
2253
+
2254
+ export { COLOR_VALUE_DOMAINS, RenderGraph, RenderGraphBuilder, RenderGraphError, createCurrentFrameObservationLease, deserializeColorResourceDescriptor, deserializeColorValueDomain, isColorValueDomain, serializeColorResourceDescriptor, serializeColorValueDomain, validateColorDomainConnection };
2255
+ //# sourceMappingURL=index.mjs.map
2256
+ //# sourceMappingURL=index.mjs.map