@forgeax/engine-render-graph 0.1.7 → 0.1.20

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
@@ -57,7 +57,7 @@ function resolutionError(label, cause) {
57
57
  });
58
58
  }
59
59
  var CompiledRenderGraphImpl = class {
60
- constructor(generation, owner, device, resources, views, passes, info) {
60
+ constructor(generation, owner, device, resources, views, passes, info, colorTargetDescriptors) {
61
61
  this.generation = generation;
62
62
  this.owner = owner;
63
63
  this.device = device;
@@ -65,6 +65,7 @@ var CompiledRenderGraphImpl = class {
65
65
  this.views = views;
66
66
  this.passes = passes;
67
67
  this.info = info;
68
+ this.colorTargetDescriptors = colorTargetDescriptors;
68
69
  }
69
70
  generation;
70
71
  owner;
@@ -73,11 +74,28 @@ var CompiledRenderGraphImpl = class {
73
74
  views;
74
75
  passes;
75
76
  info;
77
+ colorTargetDescriptors;
76
78
  retired = false;
77
79
  retireResult;
78
80
  inspect() {
79
81
  return this.info;
80
82
  }
83
+ getColorTargetView(name) {
84
+ for (const view of this.views.values()) {
85
+ const resource = this.resources.get(view.record.textureId);
86
+ if (resource?.record.label === name) return view.view;
87
+ }
88
+ return void 0;
89
+ }
90
+ getColorTargetTexture(name) {
91
+ for (const resource of this.resources.values()) {
92
+ if (resource.record.label === name) return resource.texture;
93
+ }
94
+ return void 0;
95
+ }
96
+ getColorTargetDescriptor(name) {
97
+ return this.colorTargetDescriptors.get(name);
98
+ }
81
99
  execute(frame, runPass) {
82
100
  if (this.retired) {
83
101
  return err(
@@ -346,6 +364,109 @@ var CompiledRenderGraphImpl = class {
346
364
  }
347
365
  };
348
366
 
367
+ // src/resource-registry.ts
368
+ function isTextureViewDimensionCompatible(allocationDimension, viewDimension) {
369
+ if (viewDimension === void 0) return true;
370
+ if (allocationDimension === "3d") return viewDimension === "3d";
371
+ return viewDimension !== "3d";
372
+ }
373
+ var ResourceRegistry = class {
374
+ resources = /* @__PURE__ */ new Map();
375
+ add(key, descriptor) {
376
+ const entry = {
377
+ key,
378
+ descriptor,
379
+ lifetime: descriptor.lifetime
380
+ };
381
+ return this.register(entry);
382
+ }
383
+ /**
384
+ * Register a color target resource (D-8).
385
+ * Same semantics as addResource with kind:'texture' plus GPU texture
386
+ * allocation metadata. Existing callers default to a transient target.
387
+ */
388
+ addColorTarget(name, desc) {
389
+ const lifetime = desc.lifetime ?? "transient";
390
+ const colorTargetMeta = {
391
+ format: desc.format,
392
+ size: desc.size,
393
+ sample: desc.sample ?? 1,
394
+ usage: desc.usage ?? 16 | 4,
395
+ // RENDER_ATTACHMENT | TEXTURE_BINDING
396
+ ...desc.domain !== void 0 ? { domain: desc.domain } : {},
397
+ ...desc.viewFormats !== void 0 ? { viewFormats: desc.viewFormats } : {}
398
+ };
399
+ const entry = {
400
+ key: name,
401
+ descriptor: { kind: "texture", lifetime },
402
+ lifetime,
403
+ colorTarget: colorTargetMeta
404
+ };
405
+ return this.register(entry);
406
+ }
407
+ /**
408
+ * Register a color target alias that folds into the source's physical
409
+ * texture at compile time (KB-1 MoveNode pattern, D-2).
410
+ * The source must already be registered via addColorTarget.
411
+ */
412
+ addColorTargetAlias(name, source) {
413
+ if (this.resources.has(name)) return this.duplicateResource(name);
414
+ const sourceMeta = this.resources.get(source)?.colorTarget;
415
+ if (sourceMeta === void 0) {
416
+ return err(
417
+ new RenderGraphError({
418
+ code: "alias-source-missing",
419
+ expected: `alias '${name}' source '${source}' must be a registered color target`,
420
+ hint: `call addColorTarget('${source}', ...) before retrying alias '${name}'`,
421
+ detail: { aliasKey: name, sourceKey: source }
422
+ })
423
+ );
424
+ }
425
+ const entry = {
426
+ key: name,
427
+ descriptor: { kind: "texture", lifetime: "transient" },
428
+ lifetime: "transient",
429
+ colorTarget: {
430
+ format: sourceMeta.format,
431
+ size: sourceMeta.size,
432
+ sample: sourceMeta.sample,
433
+ usage: sourceMeta.usage,
434
+ ...sourceMeta.domain !== void 0 ? { domain: sourceMeta.domain } : {},
435
+ ...sourceMeta.viewFormats !== void 0 ? { viewFormats: sourceMeta.viewFormats } : {},
436
+ aliasedFrom: source
437
+ }
438
+ };
439
+ return this.register(entry);
440
+ }
441
+ duplicateResource(key) {
442
+ return err(
443
+ new RenderGraphError({
444
+ code: "duplicate-resource",
445
+ expected: `resource key '${key}' registered exactly once`,
446
+ hint: `remove the duplicate resource declaration for '${key}' or use a different key`,
447
+ detail: { resourceKey: key }
448
+ })
449
+ );
450
+ }
451
+ register(entry) {
452
+ if (this.resources.has(entry.key)) return this.duplicateResource(entry.key);
453
+ this.resources.set(entry.key, entry);
454
+ return ok(entry);
455
+ }
456
+ get(key) {
457
+ return this.resources.get(key);
458
+ }
459
+ getColorTargetMeta(key) {
460
+ return this.resources.get(key)?.colorTarget;
461
+ }
462
+ has(key) {
463
+ return this.resources.has(key);
464
+ }
465
+ entries() {
466
+ return this.resources.values();
467
+ }
468
+ };
469
+
349
470
  // src/builder.ts
350
471
  var BUFFER_USAGE = {
351
472
  copySrc: 4,
@@ -364,6 +485,21 @@ var TEXTURE_USAGE = {
364
485
  renderAttachment: 16
365
486
  };
366
487
  var nextGeneration = 1;
488
+ function textureByteSize(format, extent, mipLevelCount) {
489
+ 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;
490
+ if (bytesPerTexel === void 0) return void 0;
491
+ let bytes = 0;
492
+ let width = extent.width;
493
+ let height = extent.height;
494
+ let depth = extent.depthOrArrayLayers;
495
+ for (let level = 0; level < mipLevelCount; level += 1) {
496
+ bytes += width * height * depth * bytesPerTexel;
497
+ width = Math.max(1, Math.floor(width / 2));
498
+ height = Math.max(1, Math.floor(height / 2));
499
+ depth = Math.max(1, Math.floor(depth / 2));
500
+ }
501
+ return bytes;
502
+ }
367
503
  function bufferUsage(access) {
368
504
  switch (access) {
369
505
  case "uniform-read":
@@ -392,6 +528,8 @@ function textureUsage(access) {
392
528
  case "storage-write":
393
529
  case "storage-read-write":
394
530
  return TEXTURE_USAGE.storageBinding;
531
+ case "sampled-storage-read-write":
532
+ return TEXTURE_USAGE.storageBinding | TEXTURE_USAGE.textureBinding;
395
533
  case "color-attachment":
396
534
  case "depth-stencil-read":
397
535
  case "depth-stencil-write":
@@ -405,6 +543,7 @@ function textureUsage(access) {
405
543
  function accessMode(access) {
406
544
  switch (access) {
407
545
  case "storage-read-write":
546
+ case "sampled-storage-read-write":
408
547
  return { read: true, write: true };
409
548
  case "storage-write":
410
549
  case "color-attachment":
@@ -421,7 +560,13 @@ function rangesOverlap(left, right) {
421
560
  return aspectOverlap && left.mipStart < right.mipEnd && right.mipStart < left.mipEnd && left.layerStart < right.layerEnd && right.layerStart < left.layerEnd;
422
561
  }
423
562
  function freezeInfo(info) {
563
+ const freezeDescriptor = (descriptor) => {
564
+ if (descriptor.kind !== "texture") return Object.freeze({ ...descriptor });
565
+ const size = typeof descriptor.size === "string" ? descriptor.size : Object.freeze({ ...descriptor.size });
566
+ return Object.freeze({ ...descriptor, size });
567
+ };
424
568
  return Object.freeze({
569
+ generation: info.generation,
425
570
  passes: Object.freeze(
426
571
  info.passes.map(
427
572
  (pass) => Object.freeze({
@@ -431,7 +576,14 @@ function freezeInfo(info) {
431
576
  })
432
577
  )
433
578
  ),
434
- resources: Object.freeze(info.resources.map((resource) => Object.freeze({ ...resource })))
579
+ resources: Object.freeze(
580
+ info.resources.map(
581
+ (resource) => Object.freeze({
582
+ ...resource,
583
+ descriptor: freezeDescriptor(resource.descriptor)
584
+ })
585
+ )
586
+ )
435
587
  });
436
588
  }
437
589
  var RenderGraphBuilder = class {
@@ -563,7 +715,20 @@ var RenderGraphBuilder = class {
563
715
  analyzed.value.lastUseByResource
564
716
  );
565
717
  if (!allocated.ok) return allocated;
718
+ const generation = nextGeneration;
719
+ const physicalAllocationKeys = /* @__PURE__ */ new WeakMap();
720
+ let nextPhysicalAllocationKey = 1;
721
+ const physicalKey = (resource) => {
722
+ const handle = resource.texture ?? resource.buffer;
723
+ if (handle === void 0) return void 0;
724
+ const existing = physicalAllocationKeys.get(handle);
725
+ if (existing !== void 0) return existing;
726
+ const key = `allocation-${nextPhysicalAllocationKey++}`;
727
+ physicalAllocationKeys.set(handle, key);
728
+ return key;
729
+ };
566
730
  const info = freezeInfo({
731
+ generation,
567
732
  passes: analyzed.value.passes.map((pass, executionIndex) => ({
568
733
  name: pass.name,
569
734
  kind: pass.pass.kind,
@@ -579,15 +744,60 @@ var RenderGraphBuilder = class {
579
744
  (dependency) => analyzed.value.passes[dependency]?.name ?? "unknown"
580
745
  )
581
746
  })),
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
- }))
747
+ resources: [...allocated.value.resources.values()].map((resource) => {
748
+ const texture = resource.record.kind === "texture" ? resource.record.descriptor : void 0;
749
+ const buffer = resource.record.kind === "buffer" ? resource.record.descriptor : void 0;
750
+ const allocationKey = physicalKey(resource);
751
+ const extent = texture === void 0 ? void 0 : this.resolveExtent(texture.size, options.surfaceSize);
752
+ return {
753
+ label: resource.record.label,
754
+ kind: resource.record.kind,
755
+ origin: resource.record.origin,
756
+ descriptor: texture === void 0 ? {
757
+ kind: "buffer",
758
+ size: buffer?.size ?? 0
759
+ } : {
760
+ kind: "texture",
761
+ format: texture.format,
762
+ ...texture.domain === void 0 ? {} : { domain: texture.domain },
763
+ size: texture.size,
764
+ width: extent?.width ?? 1,
765
+ height: extent?.height ?? 1,
766
+ depthOrArrayLayers: extent?.depthOrArrayLayers ?? 1,
767
+ mipLevelCount: texture.mipLevelCount ?? 1,
768
+ sampleCount: texture.sampleCount ?? 1
769
+ },
770
+ firstUse: resource.firstUse,
771
+ lastUse: resource.lastUse,
772
+ derivedUsage: resource.usage,
773
+ ...allocationKey === void 0 ? {} : { physicalAllocationKey: allocationKey },
774
+ ...texture === void 0 ? {} : { format: texture.format },
775
+ ...texture === void 0 ? buffer === void 0 ? {} : { byteSize: buffer.size } : {
776
+ byteSize: textureByteSize(
777
+ texture.format,
778
+ this.resolveExtent(texture.size, options.surfaceSize),
779
+ texture.mipLevelCount ?? 1
780
+ )
781
+ },
782
+ ...texture === void 0 ? {} : {
783
+ dimension: texture.dimension ?? "2d",
784
+ extent
785
+ }
786
+ };
787
+ })
590
788
  });
789
+ const colorTargetDescriptors = /* @__PURE__ */ new Map();
790
+ for (const resource of allocated.value.resources.values()) {
791
+ if (resource.record.kind !== "texture" || resource.texture === void 0) continue;
792
+ const extent = this.resolveExtent(resource.record.descriptor.size, options.surfaceSize);
793
+ colorTargetDescriptors.set(resource.record.label, {
794
+ texture: resource.texture,
795
+ format: resource.record.descriptor.format,
796
+ size: { width: extent.width, height: extent.height },
797
+ usage: resource.usage,
798
+ sample: resource.record.descriptor.sampleCount ?? 1
799
+ });
800
+ }
591
801
  return ok(
592
802
  new CompiledRenderGraphImpl(
593
803
  nextGeneration++,
@@ -596,7 +806,8 @@ var RenderGraphBuilder = class {
596
806
  allocated.value.resources,
597
807
  allocated.value.views,
598
808
  Object.freeze(analyzed.value.passes),
599
- info
809
+ info,
810
+ colorTargetDescriptors
600
811
  )
601
812
  );
602
813
  }
@@ -641,6 +852,11 @@ var RenderGraphBuilder = class {
641
852
  const lastUseByResource = /* @__PURE__ */ new Map();
642
853
  const compiledPasses = [];
643
854
  const history = [];
855
+ for (const resource of this.resources.values()) {
856
+ if (resource.kind !== "texture" || resource.origin !== "created") continue;
857
+ const usage = resource.descriptor.usage ?? 0;
858
+ if (usage !== 0) usageByResource.set(resource.id, usage);
859
+ }
644
860
  for (let passIndex = 0; passIndex < this.passes.length; passIndex++) {
645
861
  const pass = this.passes[passIndex];
646
862
  if (pass === void 0) continue;
@@ -822,7 +1038,8 @@ var RenderGraphBuilder = class {
822
1038
  continue;
823
1039
  }
824
1040
  if (!left.write && !right.write) continue;
825
- if (left.usage === right.usage && left.usage === "storage-read-write") continue;
1041
+ if (left.usage === right.usage && (left.usage === "storage-read-write" || left.usage === "sampled-storage-read-write"))
1042
+ continue;
826
1043
  const label = this.resources.get(left.resourceId)?.label;
827
1044
  return err(
828
1045
  new RenderGraphError({
@@ -966,6 +1183,27 @@ var RenderGraphBuilder = class {
966
1183
  }
967
1184
  }
968
1185
  }
1186
+ for (const view of this.views.values()) {
1187
+ const texture = this.resources.get(view.textureId);
1188
+ if (texture?.kind !== "texture") continue;
1189
+ const allocationDimension = texture.descriptor.dimension ?? "2d";
1190
+ const viewDimension = view.descriptor.dimension;
1191
+ if (!isTextureViewDimensionCompatible(allocationDimension, viewDimension)) {
1192
+ return err(
1193
+ new RenderGraphError({
1194
+ code: "resource-descriptor-invalid",
1195
+ expected: `texture '${texture.label}' view dimension matches allocation dimension`,
1196
+ hint: "use a 3d view only for a 3d allocation and preserve array views on 2d allocations",
1197
+ detail: {
1198
+ resourceLabel: texture.label,
1199
+ field: "dimension",
1200
+ expected: allocationDimension,
1201
+ actual: viewDimension ?? "2d"
1202
+ }
1203
+ })
1204
+ );
1205
+ }
1206
+ }
969
1207
  return ok(void 0);
970
1208
  }
971
1209
  allocate(options, usageByResource, firstUseByResource, lastUseByResource) {
@@ -1299,104 +1537,6 @@ function validateColorDomainConnection(source, destination, conversion) {
1299
1537
  };
1300
1538
  }
1301
1539
 
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
1540
  // src/graph.ts
1401
1541
  function poolKey(meta) {
1402
1542
  return `${meta.format}:${meta.width}x${meta.height}:${meta.usage}:${meta.sample}:${JSON.stringify(meta.viewFormats)}`;