@forgeax/engine-render-graph 0.1.19 → 0.1.21

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/dist/index.mjs CHANGED
@@ -96,7 +96,7 @@ var CompiledRenderGraphImpl = class {
96
96
  getColorTargetDescriptor(name) {
97
97
  return this.colorTargetDescriptors.get(name);
98
98
  }
99
- execute(frame, runPass) {
99
+ execute(frame, runPass, instrumentation) {
100
100
  if (this.retired) {
101
101
  return err(
102
102
  new RenderGraphError({
@@ -113,6 +113,8 @@ var CompiledRenderGraphImpl = class {
113
113
  const resolver = this.createPassResolver(pass, resolved.value);
114
114
  try {
115
115
  if (pass.pass.descriptor.executeIf?.(frame) === false) continue;
116
+ const execution = { name: pass.name, kind: pass.pass.kind, executionIndex };
117
+ const scope = instrumentation?.begin(execution, frame);
116
118
  let encodeResult = ok(void 0);
117
119
  const encode = () => {
118
120
  switch (pass.pass.kind) {
@@ -160,11 +162,13 @@ var CompiledRenderGraphImpl = class {
160
162
  ...attachment.stencilReadOnly === void 0 ? {} : { stencilReadOnly: attachment.stencilReadOnly }
161
163
  };
162
164
  }
163
- const encoder = frame.encoder.beginRenderPass({
165
+ const baseDescriptor = {
164
166
  label: pass.name,
165
167
  colorAttachments,
166
168
  ...depthStencilAttachment === void 0 ? {} : { depthStencilAttachment }
167
- });
169
+ };
170
+ const instrumentedDescriptor = scope?.renderPassDescriptor?.(baseDescriptor) ?? baseDescriptor;
171
+ const encoder = frame.encoder.beginRenderPass(instrumentedDescriptor);
168
172
  try {
169
173
  descriptor.encode({ pass: encoder, frame, resources: resolver });
170
174
  } finally {
@@ -174,9 +178,13 @@ var CompiledRenderGraphImpl = class {
174
178
  }
175
179
  case "compute": {
176
180
  const begin = pass.pass.descriptor.begin?.(frame);
181
+ const instrumentedBegin = scope?.computePassDescriptor?.(begin ?? {}) ?? begin;
177
182
  let encoder;
178
183
  try {
179
- encoder = frame.encoder.beginComputePass({ ...begin ?? {}, label: pass.name });
184
+ encoder = frame.encoder.beginComputePass({
185
+ ...instrumentedBegin ?? {},
186
+ label: pass.name
187
+ });
180
188
  } catch (cause) {
181
189
  pass.pass.descriptor.onBeginError?.(frame, cause);
182
190
  throw cause;
@@ -190,14 +198,19 @@ var CompiledRenderGraphImpl = class {
190
198
  break;
191
199
  }
192
200
  case "copy":
193
- pass.pass.descriptor.encode({ encoder: frame.encoder, frame, resources: resolver });
201
+ scope?.beforeCopy?.(frame.encoder);
202
+ try {
203
+ pass.pass.descriptor.encode({ encoder: frame.encoder, frame, resources: resolver });
204
+ } finally {
205
+ scope?.afterCopy?.(frame.encoder);
206
+ }
194
207
  break;
195
208
  }
196
209
  };
197
210
  if (runPass === void 0) {
198
211
  encode();
199
212
  } else {
200
- runPass({ name: pass.name, kind: pass.pass.kind, executionIndex }, encode);
213
+ runPass(execution, encode);
201
214
  }
202
215
  if (!encodeResult.ok) return encodeResult;
203
216
  } catch (cause) {
@@ -364,6 +377,109 @@ var CompiledRenderGraphImpl = class {
364
377
  }
365
378
  };
366
379
 
380
+ // src/resource-registry.ts
381
+ function isTextureViewDimensionCompatible(allocationDimension, viewDimension) {
382
+ if (viewDimension === void 0) return true;
383
+ if (allocationDimension === "3d") return viewDimension === "3d";
384
+ return viewDimension !== "3d";
385
+ }
386
+ var ResourceRegistry = class {
387
+ resources = /* @__PURE__ */ new Map();
388
+ add(key, descriptor) {
389
+ const entry = {
390
+ key,
391
+ descriptor,
392
+ lifetime: descriptor.lifetime
393
+ };
394
+ return this.register(entry);
395
+ }
396
+ /**
397
+ * Register a color target resource (D-8).
398
+ * Same semantics as addResource with kind:'texture' plus GPU texture
399
+ * allocation metadata. Existing callers default to a transient target.
400
+ */
401
+ addColorTarget(name, desc) {
402
+ const lifetime = desc.lifetime ?? "transient";
403
+ const colorTargetMeta = {
404
+ format: desc.format,
405
+ size: desc.size,
406
+ sample: desc.sample ?? 1,
407
+ usage: desc.usage ?? 16 | 4,
408
+ // RENDER_ATTACHMENT | TEXTURE_BINDING
409
+ ...desc.domain !== void 0 ? { domain: desc.domain } : {},
410
+ ...desc.viewFormats !== void 0 ? { viewFormats: desc.viewFormats } : {}
411
+ };
412
+ const entry = {
413
+ key: name,
414
+ descriptor: { kind: "texture", lifetime },
415
+ lifetime,
416
+ colorTarget: colorTargetMeta
417
+ };
418
+ return this.register(entry);
419
+ }
420
+ /**
421
+ * Register a color target alias that folds into the source's physical
422
+ * texture at compile time (KB-1 MoveNode pattern, D-2).
423
+ * The source must already be registered via addColorTarget.
424
+ */
425
+ addColorTargetAlias(name, source) {
426
+ if (this.resources.has(name)) return this.duplicateResource(name);
427
+ const sourceMeta = this.resources.get(source)?.colorTarget;
428
+ if (sourceMeta === void 0) {
429
+ return err(
430
+ new RenderGraphError({
431
+ code: "alias-source-missing",
432
+ expected: `alias '${name}' source '${source}' must be a registered color target`,
433
+ hint: `call addColorTarget('${source}', ...) before retrying alias '${name}'`,
434
+ detail: { aliasKey: name, sourceKey: source }
435
+ })
436
+ );
437
+ }
438
+ const entry = {
439
+ key: name,
440
+ descriptor: { kind: "texture", lifetime: "transient" },
441
+ lifetime: "transient",
442
+ colorTarget: {
443
+ format: sourceMeta.format,
444
+ size: sourceMeta.size,
445
+ sample: sourceMeta.sample,
446
+ usage: sourceMeta.usage,
447
+ ...sourceMeta.domain !== void 0 ? { domain: sourceMeta.domain } : {},
448
+ ...sourceMeta.viewFormats !== void 0 ? { viewFormats: sourceMeta.viewFormats } : {},
449
+ aliasedFrom: source
450
+ }
451
+ };
452
+ return this.register(entry);
453
+ }
454
+ duplicateResource(key) {
455
+ return err(
456
+ new RenderGraphError({
457
+ code: "duplicate-resource",
458
+ expected: `resource key '${key}' registered exactly once`,
459
+ hint: `remove the duplicate resource declaration for '${key}' or use a different key`,
460
+ detail: { resourceKey: key }
461
+ })
462
+ );
463
+ }
464
+ register(entry) {
465
+ if (this.resources.has(entry.key)) return this.duplicateResource(entry.key);
466
+ this.resources.set(entry.key, entry);
467
+ return ok(entry);
468
+ }
469
+ get(key) {
470
+ return this.resources.get(key);
471
+ }
472
+ getColorTargetMeta(key) {
473
+ return this.resources.get(key)?.colorTarget;
474
+ }
475
+ has(key) {
476
+ return this.resources.has(key);
477
+ }
478
+ entries() {
479
+ return this.resources.values();
480
+ }
481
+ };
482
+
367
483
  // src/builder.ts
368
484
  var BUFFER_USAGE = {
369
485
  copySrc: 4,
@@ -382,6 +498,21 @@ var TEXTURE_USAGE = {
382
498
  renderAttachment: 16
383
499
  };
384
500
  var nextGeneration = 1;
501
+ function textureByteSize(format, extent, mipLevelCount) {
502
+ const bytesPerTexel = format === "r8unorm" || format === "r8snorm" || format === "r8uint" || format === "r8sint" ? 1 : format === "rg8unorm" || format === "rg8snorm" || format === "rg8uint" || format === "rg8sint" ? 2 : format === "rgba8unorm" || format === "rgba8unorm-srgb" || format === "rgba8snorm" || format === "rgba8uint" || format === "rgba8sint" || format === "r32float" || format === "r32uint" || format === "r32sint" ? 4 : format === "rg16float" || format === "rg16uint" || format === "rg16sint" || format === "rg16snorm" || format === "rg16unorm" ? 4 : format === "rgba16float" || format === "rgba16uint" || format === "rgba16sint" || format === "rgba16snorm" || format === "rgba16unorm" || format === "rg32float" || format === "rg32uint" || format === "rg32sint" ? 8 : format === "rgba32float" || format === "rgba32uint" || format === "rgba32sint" ? 16 : void 0;
503
+ if (bytesPerTexel === void 0) return void 0;
504
+ let bytes = 0;
505
+ let width = extent.width;
506
+ let height = extent.height;
507
+ let depth = extent.depthOrArrayLayers;
508
+ for (let level = 0; level < mipLevelCount; level += 1) {
509
+ bytes += width * height * depth * bytesPerTexel;
510
+ width = Math.max(1, Math.floor(width / 2));
511
+ height = Math.max(1, Math.floor(height / 2));
512
+ depth = Math.max(1, Math.floor(depth / 2));
513
+ }
514
+ return bytes;
515
+ }
385
516
  function bufferUsage(access) {
386
517
  switch (access) {
387
518
  case "uniform-read":
@@ -410,6 +541,8 @@ function textureUsage(access) {
410
541
  case "storage-write":
411
542
  case "storage-read-write":
412
543
  return TEXTURE_USAGE.storageBinding;
544
+ case "sampled-storage-read-write":
545
+ return TEXTURE_USAGE.storageBinding | TEXTURE_USAGE.textureBinding;
413
546
  case "color-attachment":
414
547
  case "depth-stencil-read":
415
548
  case "depth-stencil-write":
@@ -423,6 +556,7 @@ function textureUsage(access) {
423
556
  function accessMode(access) {
424
557
  switch (access) {
425
558
  case "storage-read-write":
559
+ case "sampled-storage-read-write":
426
560
  return { read: true, write: true };
427
561
  case "storage-write":
428
562
  case "color-attachment":
@@ -445,6 +579,7 @@ function freezeInfo(info) {
445
579
  return Object.freeze({ ...descriptor, size });
446
580
  };
447
581
  return Object.freeze({
582
+ generation: info.generation,
448
583
  passes: Object.freeze(
449
584
  info.passes.map(
450
585
  (pass) => Object.freeze({
@@ -593,7 +728,20 @@ var RenderGraphBuilder = class {
593
728
  analyzed.value.lastUseByResource
594
729
  );
595
730
  if (!allocated.ok) return allocated;
731
+ const generation = nextGeneration;
732
+ const physicalAllocationKeys = /* @__PURE__ */ new WeakMap();
733
+ let nextPhysicalAllocationKey = 1;
734
+ const physicalKey = (resource) => {
735
+ const handle = resource.texture ?? resource.buffer;
736
+ if (handle === void 0) return void 0;
737
+ const existing = physicalAllocationKeys.get(handle);
738
+ if (existing !== void 0) return existing;
739
+ const key = `allocation-${nextPhysicalAllocationKey++}`;
740
+ physicalAllocationKeys.set(handle, key);
741
+ return key;
742
+ };
596
743
  const info = freezeInfo({
744
+ generation,
597
745
  passes: analyzed.value.passes.map((pass, executionIndex) => ({
598
746
  name: pass.name,
599
747
  kind: pass.pass.kind,
@@ -609,26 +757,47 @@ var RenderGraphBuilder = class {
609
757
  (dependency) => analyzed.value.passes[dependency]?.name ?? "unknown"
610
758
  )
611
759
  })),
612
- resources: [...allocated.value.resources.values()].map((resource) => ({
613
- label: resource.record.label,
614
- kind: resource.record.kind,
615
- origin: resource.record.origin,
616
- descriptor: resource.record.kind === "texture" ? {
617
- kind: "texture",
618
- format: resource.record.descriptor.format,
619
- ...resource.record.descriptor.domain === void 0 ? {} : { domain: resource.record.descriptor.domain },
620
- size: resource.record.descriptor.size,
621
- ...this.resolveExtent(resource.record.descriptor.size, options.surfaceSize),
622
- mipLevelCount: resource.record.descriptor.mipLevelCount ?? 1,
623
- sampleCount: resource.record.descriptor.sampleCount ?? 1
624
- } : {
625
- kind: "buffer",
626
- size: resource.record.descriptor.size
627
- },
628
- firstUse: resource.firstUse,
629
- lastUse: resource.lastUse,
630
- derivedUsage: resource.usage
631
- }))
760
+ resources: [...allocated.value.resources.values()].map((resource) => {
761
+ const texture = resource.record.kind === "texture" ? resource.record.descriptor : void 0;
762
+ const buffer = resource.record.kind === "buffer" ? resource.record.descriptor : void 0;
763
+ const allocationKey = physicalKey(resource);
764
+ const extent = texture === void 0 ? void 0 : this.resolveExtent(texture.size, options.surfaceSize);
765
+ return {
766
+ label: resource.record.label,
767
+ kind: resource.record.kind,
768
+ origin: resource.record.origin,
769
+ descriptor: texture === void 0 ? {
770
+ kind: "buffer",
771
+ size: buffer?.size ?? 0
772
+ } : {
773
+ kind: "texture",
774
+ format: texture.format,
775
+ ...texture.domain === void 0 ? {} : { domain: texture.domain },
776
+ size: texture.size,
777
+ width: extent?.width ?? 1,
778
+ height: extent?.height ?? 1,
779
+ depthOrArrayLayers: extent?.depthOrArrayLayers ?? 1,
780
+ mipLevelCount: texture.mipLevelCount ?? 1,
781
+ sampleCount: texture.sampleCount ?? 1
782
+ },
783
+ firstUse: resource.firstUse,
784
+ lastUse: resource.lastUse,
785
+ derivedUsage: resource.usage,
786
+ ...allocationKey === void 0 ? {} : { physicalAllocationKey: allocationKey },
787
+ ...texture === void 0 ? {} : { format: texture.format },
788
+ ...texture === void 0 ? buffer === void 0 ? {} : { byteSize: buffer.size } : {
789
+ byteSize: textureByteSize(
790
+ texture.format,
791
+ this.resolveExtent(texture.size, options.surfaceSize),
792
+ texture.mipLevelCount ?? 1
793
+ )
794
+ },
795
+ ...texture === void 0 ? {} : {
796
+ dimension: texture.dimension ?? "2d",
797
+ extent
798
+ }
799
+ };
800
+ })
632
801
  });
633
802
  const colorTargetDescriptors = /* @__PURE__ */ new Map();
634
803
  for (const resource of allocated.value.resources.values()) {
@@ -711,7 +880,8 @@ var RenderGraphBuilder = class {
711
880
  const item = this.normalizeAccess(passIndex, pass, access);
712
881
  if (!item.ok) return item;
713
882
  normalized.push(item.value);
714
- const usage = this.resources.get(item.value.resourceId)?.kind === "buffer" ? bufferUsage(access.usage) : textureUsage(access.usage);
883
+ const resource = this.resources.get(item.value.resourceId);
884
+ const usage = (resource?.kind === "texture" && resource.origin === "created" ? resource.descriptor.usage ?? 0 : 0) | (resource?.kind === "buffer" ? bufferUsage(access.usage) : textureUsage(access.usage));
715
885
  usageByResource.set(
716
886
  item.value.resourceId,
717
887
  (usageByResource.get(item.value.resourceId) ?? 0) | usage
@@ -882,7 +1052,8 @@ var RenderGraphBuilder = class {
882
1052
  continue;
883
1053
  }
884
1054
  if (!left.write && !right.write) continue;
885
- if (left.usage === right.usage && left.usage === "storage-read-write") continue;
1055
+ if (left.usage === right.usage && (left.usage === "storage-read-write" || left.usage === "sampled-storage-read-write"))
1056
+ continue;
886
1057
  const label = this.resources.get(left.resourceId)?.label;
887
1058
  return err(
888
1059
  new RenderGraphError({
@@ -1026,6 +1197,27 @@ var RenderGraphBuilder = class {
1026
1197
  }
1027
1198
  }
1028
1199
  }
1200
+ for (const view of this.views.values()) {
1201
+ const texture = this.resources.get(view.textureId);
1202
+ if (texture?.kind !== "texture") continue;
1203
+ const allocationDimension = texture.descriptor.dimension ?? "2d";
1204
+ const viewDimension = view.descriptor.dimension;
1205
+ if (!isTextureViewDimensionCompatible(allocationDimension, viewDimension)) {
1206
+ return err(
1207
+ new RenderGraphError({
1208
+ code: "resource-descriptor-invalid",
1209
+ expected: `texture '${texture.label}' view dimension matches allocation dimension`,
1210
+ hint: "use a 3d view only for a 3d allocation and preserve array views on 2d allocations",
1211
+ detail: {
1212
+ resourceLabel: texture.label,
1213
+ field: "dimension",
1214
+ expected: allocationDimension,
1215
+ actual: viewDimension ?? "2d"
1216
+ }
1217
+ })
1218
+ );
1219
+ }
1220
+ }
1029
1221
  return ok(void 0);
1030
1222
  }
1031
1223
  allocate(options, usageByResource, firstUseByResource, lastUseByResource) {
@@ -1359,104 +1551,6 @@ function validateColorDomainConnection(source, destination, conversion) {
1359
1551
  };
1360
1552
  }
1361
1553
 
1362
- // src/resource-registry.ts
1363
- var ResourceRegistry = class {
1364
- resources = /* @__PURE__ */ new Map();
1365
- add(key, descriptor) {
1366
- const entry = {
1367
- key,
1368
- descriptor,
1369
- lifetime: descriptor.lifetime
1370
- };
1371
- return this.register(entry);
1372
- }
1373
- /**
1374
- * Register a color target resource (D-8).
1375
- * Same semantics as addResource with kind:'texture' plus GPU texture
1376
- * allocation metadata. Existing callers default to a transient target.
1377
- */
1378
- addColorTarget(name, desc) {
1379
- const lifetime = desc.lifetime ?? "transient";
1380
- const colorTargetMeta = {
1381
- format: desc.format,
1382
- size: desc.size,
1383
- sample: desc.sample ?? 1,
1384
- usage: desc.usage ?? 16 | 4,
1385
- // RENDER_ATTACHMENT | TEXTURE_BINDING
1386
- ...desc.domain !== void 0 ? { domain: desc.domain } : {},
1387
- ...desc.viewFormats !== void 0 ? { viewFormats: desc.viewFormats } : {}
1388
- };
1389
- const entry = {
1390
- key: name,
1391
- descriptor: { kind: "texture", lifetime },
1392
- lifetime,
1393
- colorTarget: colorTargetMeta
1394
- };
1395
- return this.register(entry);
1396
- }
1397
- /**
1398
- * Register a color target alias that folds into the source's physical
1399
- * texture at compile time (KB-1 MoveNode pattern, D-2).
1400
- * The source must already be registered via addColorTarget.
1401
- */
1402
- addColorTargetAlias(name, source) {
1403
- if (this.resources.has(name)) return this.duplicateResource(name);
1404
- const sourceMeta = this.resources.get(source)?.colorTarget;
1405
- if (sourceMeta === void 0) {
1406
- return err(
1407
- new RenderGraphError({
1408
- code: "alias-source-missing",
1409
- expected: `alias '${name}' source '${source}' must be a registered color target`,
1410
- hint: `call addColorTarget('${source}', ...) before retrying alias '${name}'`,
1411
- detail: { aliasKey: name, sourceKey: source }
1412
- })
1413
- );
1414
- }
1415
- const entry = {
1416
- key: name,
1417
- descriptor: { kind: "texture", lifetime: "transient" },
1418
- lifetime: "transient",
1419
- colorTarget: {
1420
- format: sourceMeta.format,
1421
- size: sourceMeta.size,
1422
- sample: sourceMeta.sample,
1423
- usage: sourceMeta.usage,
1424
- ...sourceMeta.domain !== void 0 ? { domain: sourceMeta.domain } : {},
1425
- ...sourceMeta.viewFormats !== void 0 ? { viewFormats: sourceMeta.viewFormats } : {},
1426
- aliasedFrom: source
1427
- }
1428
- };
1429
- return this.register(entry);
1430
- }
1431
- duplicateResource(key) {
1432
- return err(
1433
- new RenderGraphError({
1434
- code: "duplicate-resource",
1435
- expected: `resource key '${key}' registered exactly once`,
1436
- hint: `remove the duplicate resource declaration for '${key}' or use a different key`,
1437
- detail: { resourceKey: key }
1438
- })
1439
- );
1440
- }
1441
- register(entry) {
1442
- if (this.resources.has(entry.key)) return this.duplicateResource(entry.key);
1443
- this.resources.set(entry.key, entry);
1444
- return ok(entry);
1445
- }
1446
- get(key) {
1447
- return this.resources.get(key);
1448
- }
1449
- getColorTargetMeta(key) {
1450
- return this.resources.get(key)?.colorTarget;
1451
- }
1452
- has(key) {
1453
- return this.resources.has(key);
1454
- }
1455
- entries() {
1456
- return this.resources.values();
1457
- }
1458
- };
1459
-
1460
1554
  // src/graph.ts
1461
1555
  function poolKey(meta) {
1462
1556
  return `${meta.format}:${meta.width}x${meta.height}:${meta.usage}:${meta.sample}:${JSON.stringify(meta.viewFormats)}`;