@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/src/builder.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  textureHandle,
16
16
  textureViewHandle,
17
17
  } from './kernel-internal.js';
18
+ import { isTextureViewDimensionCompatible } from './resource-registry.js';
18
19
  import type {
19
20
  CompiledRenderGraph,
20
21
  CompiledRenderGraphInfo,
@@ -60,6 +61,60 @@ const TEXTURE_USAGE = {
60
61
 
61
62
  let nextGeneration = 1;
62
63
 
64
+ function textureByteSize(
65
+ format: GPUTextureFormat,
66
+ extent: { readonly width: number; readonly height: number; readonly depthOrArrayLayers: number },
67
+ mipLevelCount: number,
68
+ ): number | undefined {
69
+ const bytesPerTexel =
70
+ format === 'r8unorm' || format === 'r8snorm' || format === 'r8uint' || format === 'r8sint'
71
+ ? 1
72
+ : format === 'rg8unorm' ||
73
+ format === 'rg8snorm' ||
74
+ format === 'rg8uint' ||
75
+ format === 'rg8sint'
76
+ ? 2
77
+ : format === 'rgba8unorm' ||
78
+ format === 'rgba8unorm-srgb' ||
79
+ format === 'rgba8snorm' ||
80
+ format === 'rgba8uint' ||
81
+ format === 'rgba8sint' ||
82
+ format === 'r32float' ||
83
+ format === 'r32uint' ||
84
+ format === 'r32sint'
85
+ ? 4
86
+ : format === 'rg16float' ||
87
+ format === 'rg16uint' ||
88
+ format === 'rg16sint' ||
89
+ format === 'rg16snorm' ||
90
+ format === 'rg16unorm'
91
+ ? 4
92
+ : format === 'rgba16float' ||
93
+ format === 'rgba16uint' ||
94
+ format === 'rgba16sint' ||
95
+ format === 'rgba16snorm' ||
96
+ format === 'rgba16unorm' ||
97
+ format === 'rg32float' ||
98
+ format === 'rg32uint' ||
99
+ format === 'rg32sint'
100
+ ? 8
101
+ : format === 'rgba32float' || format === 'rgba32uint' || format === 'rgba32sint'
102
+ ? 16
103
+ : undefined;
104
+ if (bytesPerTexel === undefined) return undefined;
105
+ let bytes = 0;
106
+ let width = extent.width;
107
+ let height = extent.height;
108
+ let depth = extent.depthOrArrayLayers;
109
+ for (let level = 0; level < mipLevelCount; level += 1) {
110
+ bytes += width * height * depth * bytesPerTexel;
111
+ width = Math.max(1, Math.floor(width / 2));
112
+ height = Math.max(1, Math.floor(height / 2));
113
+ depth = Math.max(1, Math.floor(depth / 2));
114
+ }
115
+ return bytes;
116
+ }
117
+
63
118
  interface NormalizedRange {
64
119
  readonly mipStart: number;
65
120
  readonly mipEnd: number;
@@ -108,6 +163,8 @@ function textureUsage(access: GraphTextureAccess): number {
108
163
  case 'storage-write':
109
164
  case 'storage-read-write':
110
165
  return TEXTURE_USAGE.storageBinding;
166
+ case 'sampled-storage-read-write':
167
+ return TEXTURE_USAGE.storageBinding | TEXTURE_USAGE.textureBinding;
111
168
  case 'color-attachment':
112
169
  case 'depth-stencil-read':
113
170
  case 'depth-stencil-write':
@@ -125,6 +182,7 @@ function accessMode(access: GraphBufferAccess | GraphTextureAccess): {
125
182
  } {
126
183
  switch (access) {
127
184
  case 'storage-read-write':
185
+ case 'sampled-storage-read-write':
128
186
  return { read: true, write: true };
129
187
  case 'storage-write':
130
188
  case 'color-attachment':
@@ -160,6 +218,7 @@ function freezeInfo(info: CompiledRenderGraphInfo): CompiledRenderGraphInfo {
160
218
  return Object.freeze({ ...descriptor, size });
161
219
  };
162
220
  return Object.freeze({
221
+ generation: info.generation,
163
222
  passes: Object.freeze(
164
223
  info.passes.map((pass) =>
165
224
  Object.freeze({
@@ -351,7 +410,20 @@ export class RenderGraphBuilder<FrameCtx extends RenderGraphFrame> {
351
410
  );
352
411
  if (!allocated.ok) return allocated;
353
412
 
413
+ const generation = nextGeneration;
414
+ const physicalAllocationKeys = new WeakMap<object, string>();
415
+ let nextPhysicalAllocationKey = 1;
416
+ const physicalKey = (resource: CompiledResource<FrameCtx>): string | undefined => {
417
+ const handle = resource.texture ?? resource.buffer;
418
+ if (handle === undefined) return undefined;
419
+ const existing = physicalAllocationKeys.get(handle);
420
+ if (existing !== undefined) return existing;
421
+ const key = `allocation-${nextPhysicalAllocationKey++}`;
422
+ physicalAllocationKeys.set(handle, key);
423
+ return key;
424
+ };
354
425
  const info = freezeInfo({
426
+ generation,
355
427
  passes: analyzed.value.passes.map((pass, executionIndex) => ({
356
428
  name: pass.name,
357
429
  kind: pass.pass.kind,
@@ -367,31 +439,63 @@ export class RenderGraphBuilder<FrameCtx extends RenderGraphFrame> {
367
439
  (dependency) => analyzed.value.passes[dependency]?.name ?? 'unknown',
368
440
  ),
369
441
  })),
370
- resources: [...allocated.value.resources.values()].map((resource) => ({
371
- label: resource.record.label,
372
- kind: resource.record.kind,
373
- origin: resource.record.origin,
374
- descriptor:
442
+ resources: [...allocated.value.resources.values()].map((resource) => {
443
+ const texture =
375
444
  resource.record.kind === 'texture'
376
- ? {
377
- kind: 'texture' as const,
378
- format: resource.record.descriptor.format,
379
- ...(resource.record.descriptor.domain === undefined
380
- ? {}
381
- : { domain: resource.record.descriptor.domain }),
382
- size: resource.record.descriptor.size,
383
- ...this.resolveExtent(resource.record.descriptor.size, options.surfaceSize),
384
- mipLevelCount: resource.record.descriptor.mipLevelCount ?? 1,
385
- sampleCount: resource.record.descriptor.sampleCount ?? 1,
386
- }
445
+ ? (resource.record.descriptor as GraphTextureDescriptor)
446
+ : undefined;
447
+ const buffer =
448
+ resource.record.kind === 'buffer'
449
+ ? (resource.record.descriptor as GraphBufferDescriptor)
450
+ : undefined;
451
+ const allocationKey = physicalKey(resource);
452
+ const extent =
453
+ texture === undefined ? undefined : this.resolveExtent(texture.size, options.surfaceSize);
454
+ return {
455
+ label: resource.record.label,
456
+ kind: resource.record.kind,
457
+ origin: resource.record.origin,
458
+ descriptor:
459
+ texture === undefined
460
+ ? {
461
+ kind: 'buffer' as const,
462
+ size: buffer?.size ?? 0,
463
+ }
464
+ : {
465
+ kind: 'texture' as const,
466
+ format: texture.format,
467
+ ...(texture.domain === undefined ? {} : { domain: texture.domain }),
468
+ size: texture.size,
469
+ width: extent?.width ?? 1,
470
+ height: extent?.height ?? 1,
471
+ depthOrArrayLayers: extent?.depthOrArrayLayers ?? 1,
472
+ mipLevelCount: texture.mipLevelCount ?? 1,
473
+ sampleCount: texture.sampleCount ?? 1,
474
+ },
475
+ firstUse: resource.firstUse,
476
+ lastUse: resource.lastUse,
477
+ derivedUsage: resource.usage,
478
+ ...(allocationKey === undefined ? {} : { physicalAllocationKey: allocationKey }),
479
+ ...(texture === undefined ? {} : { format: texture.format }),
480
+ ...(texture === undefined
481
+ ? buffer === undefined
482
+ ? {}
483
+ : { byteSize: buffer.size }
387
484
  : {
388
- kind: 'buffer' as const,
389
- size: resource.record.descriptor.size,
390
- },
391
- firstUse: resource.firstUse,
392
- lastUse: resource.lastUse,
393
- derivedUsage: resource.usage,
394
- })),
485
+ byteSize: textureByteSize(
486
+ texture.format,
487
+ this.resolveExtent(texture.size, options.surfaceSize),
488
+ texture.mipLevelCount ?? 1,
489
+ ),
490
+ }),
491
+ ...(texture === undefined
492
+ ? {}
493
+ : {
494
+ dimension: texture.dimension ?? '2d',
495
+ extent,
496
+ }),
497
+ };
498
+ }),
395
499
  });
396
500
 
397
501
  const colorTargetDescriptors = new Map<string, ResolvedColorTargetDescriptor>();
@@ -493,10 +597,14 @@ export class RenderGraphBuilder<FrameCtx extends RenderGraphFrame> {
493
597
  const item = this.normalizeAccess(passIndex, pass, access);
494
598
  if (!item.ok) return item;
495
599
  normalized.push(item.value);
600
+ const resource = this.resources.get(item.value.resourceId);
496
601
  const usage =
497
- this.resources.get(item.value.resourceId)?.kind === 'buffer'
602
+ (resource?.kind === 'texture' && resource.origin === 'created'
603
+ ? (resource.descriptor.usage ?? 0)
604
+ : 0) |
605
+ (resource?.kind === 'buffer'
498
606
  ? bufferUsage(access.usage as GraphBufferAccess)
499
- : textureUsage(access.usage as GraphTextureAccess);
607
+ : textureUsage(access.usage as GraphTextureAccess));
500
608
  usageByResource.set(
501
609
  item.value.resourceId,
502
610
  (usageByResource.get(item.value.resourceId) ?? 0) | usage,
@@ -717,7 +825,11 @@ export class RenderGraphBuilder<FrameCtx extends RenderGraphFrame> {
717
825
  continue;
718
826
  }
719
827
  if (!left.write && !right.write) continue;
720
- if (left.usage === right.usage && left.usage === 'storage-read-write') continue;
828
+ if (
829
+ left.usage === right.usage &&
830
+ (left.usage === 'storage-read-write' || left.usage === 'sampled-storage-read-write')
831
+ )
832
+ continue;
721
833
  const label = this.resources.get(left.resourceId)?.label;
722
834
  return err(
723
835
  new RenderGraphError({
@@ -891,6 +1003,27 @@ export class RenderGraphBuilder<FrameCtx extends RenderGraphFrame> {
891
1003
  }
892
1004
  }
893
1005
  }
1006
+ for (const view of this.views.values()) {
1007
+ const texture = this.resources.get(view.textureId);
1008
+ if (texture?.kind !== 'texture') continue;
1009
+ const allocationDimension = texture.descriptor.dimension ?? '2d';
1010
+ const viewDimension = view.descriptor.dimension;
1011
+ if (!isTextureViewDimensionCompatible(allocationDimension, viewDimension)) {
1012
+ return err(
1013
+ new RenderGraphError({
1014
+ code: 'resource-descriptor-invalid',
1015
+ expected: `texture '${texture.label}' view dimension matches allocation dimension`,
1016
+ hint: 'use a 3d view only for a 3d allocation and preserve array views on 2d allocations',
1017
+ detail: {
1018
+ resourceLabel: texture.label,
1019
+ field: 'dimension',
1020
+ expected: allocationDimension,
1021
+ actual: viewDimension ?? '2d',
1022
+ },
1023
+ }),
1024
+ );
1025
+ }
1026
+ }
894
1027
  return ok(undefined);
895
1028
  }
896
1029
 
@@ -24,6 +24,7 @@ import type {
24
24
  GraphTexture,
25
25
  GraphTextureView,
26
26
  RenderGraphFrame,
27
+ RenderGraphPassInstrumentation,
27
28
  RenderGraphPassRunner,
28
29
  } from './types.js';
29
30
 
@@ -82,7 +83,11 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
82
83
  return this.colorTargetDescriptors.get(name);
83
84
  }
84
85
 
85
- execute(frame: FrameCtx, runPass?: RenderGraphPassRunner): Result<void, RenderGraphError> {
86
+ execute(
87
+ frame: FrameCtx,
88
+ runPass?: RenderGraphPassRunner,
89
+ instrumentation?: RenderGraphPassInstrumentation<FrameCtx>,
90
+ ): Result<void, RenderGraphError> {
86
91
  if (this.retired) {
87
92
  return err(
88
93
  new RenderGraphError({
@@ -101,6 +106,8 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
101
106
  const resolver = this.createPassResolver(pass, resolved.value);
102
107
  try {
103
108
  if (pass.pass.descriptor.executeIf?.(frame) === false) continue;
109
+ const execution = { name: pass.name, kind: pass.pass.kind, executionIndex };
110
+ const scope = instrumentation?.begin(execution, frame);
104
111
  let encodeResult: Result<void, RenderGraphError> = ok(undefined);
105
112
  const encode = () => {
106
113
  switch (pass.pass.kind) {
@@ -172,11 +179,14 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
172
179
  : { stencilReadOnly: attachment.stencilReadOnly }),
173
180
  };
174
181
  }
175
- const encoder = frame.encoder.beginRenderPass({
182
+ const baseDescriptor = {
176
183
  label: pass.name,
177
184
  colorAttachments,
178
185
  ...(depthStencilAttachment === undefined ? {} : { depthStencilAttachment }),
179
- });
186
+ };
187
+ const instrumentedDescriptor =
188
+ scope?.renderPassDescriptor?.(baseDescriptor) ?? baseDescriptor;
189
+ const encoder = frame.encoder.beginRenderPass(instrumentedDescriptor);
180
190
  try {
181
191
  descriptor.encode({ pass: encoder, frame, resources: resolver });
182
192
  } finally {
@@ -186,9 +196,13 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
186
196
  }
187
197
  case 'compute': {
188
198
  const begin = pass.pass.descriptor.begin?.(frame);
199
+ const instrumentedBegin = scope?.computePassDescriptor?.(begin ?? {}) ?? begin;
189
200
  let encoder: RhiComputePassEncoder;
190
201
  try {
191
- encoder = frame.encoder.beginComputePass({ ...(begin ?? {}), label: pass.name });
202
+ encoder = frame.encoder.beginComputePass({
203
+ ...(instrumentedBegin ?? {}),
204
+ label: pass.name,
205
+ });
192
206
  } catch (cause) {
193
207
  pass.pass.descriptor.onBeginError?.(frame, cause);
194
208
  throw cause;
@@ -202,14 +216,19 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
202
216
  break;
203
217
  }
204
218
  case 'copy':
205
- pass.pass.descriptor.encode({ encoder: frame.encoder, frame, resources: resolver });
219
+ scope?.beforeCopy?.(frame.encoder);
220
+ try {
221
+ pass.pass.descriptor.encode({ encoder: frame.encoder, frame, resources: resolver });
222
+ } finally {
223
+ scope?.afterCopy?.(frame.encoder);
224
+ }
206
225
  break;
207
226
  }
208
227
  };
209
228
  if (runPass === undefined) {
210
229
  encode();
211
230
  } else {
212
- runPass({ name: pass.name, kind: pass.pass.kind, executionIndex }, encode);
231
+ runPass(execution, encode);
213
232
  }
214
233
  if (!encodeResult.ok) return encodeResult;
215
234
  } catch (cause) {
package/src/index.ts CHANGED
@@ -97,5 +97,7 @@ export type {
97
97
  RenderGraphCompileOptions,
98
98
  RenderGraphFrame,
99
99
  RenderGraphPassExecution,
100
+ RenderGraphPassInstrumentation,
101
+ RenderGraphPassInstrumentationScope,
100
102
  RenderGraphPassRunner,
101
103
  } from './types.js';
@@ -26,6 +26,16 @@ import type {
26
26
  } from './graph.js';
27
27
  import type { ColorValueDomain } from './pipeline/color-value-domain.js';
28
28
 
29
+ /** Check the only allocation/view dimension combinations supported by WebGPU. */
30
+ export function isTextureViewDimensionCompatible(
31
+ allocationDimension: GPUTextureDimension,
32
+ viewDimension: GPUTextureViewDimension | undefined,
33
+ ): boolean {
34
+ if (viewDimension === undefined) return true;
35
+ if (allocationDimension === '3d') return viewDimension === '3d';
36
+ return viewDimension !== '3d';
37
+ }
38
+
29
39
  /**
30
40
  * Per-resource GPU allocation metadata carried through compile.
31
41
  * When the resource was registered via addColorTarget, colorTarget
package/src/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  Buffer,
3
3
  ComputePassDescriptor,
4
+ RenderPassDescriptor,
4
5
  RhiCommandEncoder,
5
6
  RhiComputePassEncoder,
6
7
  RhiDevice,
@@ -45,7 +46,7 @@ export type GraphExtent =
45
46
  export interface GraphTextureDescriptor {
46
47
  readonly format: TextureFormat;
47
48
  readonly size: GraphExtent;
48
- /** Additional RHI usage required by an external diagnostic consumer. */
49
+ /** Additional RHI usage required by an external diagnostic consumer such as readback. */
49
50
  readonly usage?: number | undefined;
50
51
  readonly mipLevelCount?: number | undefined;
51
52
  readonly sampleCount?: number | undefined;
@@ -97,6 +98,7 @@ export type GraphTextureAccess =
97
98
  | 'storage-read'
98
99
  | 'storage-write'
99
100
  | 'storage-read-write'
101
+ | 'sampled-storage-read-write'
100
102
  | 'color-attachment'
101
103
  | 'depth-stencil-read'
102
104
  | 'depth-stencil-write'
@@ -215,7 +217,20 @@ export interface CompiledRenderGraphInfo {
215
217
  readonly firstUse: number | null;
216
218
  readonly lastUse: number | null;
217
219
  readonly derivedUsage: number;
220
+ /** Stable compile-local identity of the physical RHI allocation. */
221
+ readonly physicalAllocationKey?: string | undefined;
222
+ /** Descriptor-derived byte size when the format is uncompressed and known. */
223
+ readonly byteSize?: number | undefined;
224
+ /** Texture format retained for producer-owned resource inspection. */
225
+ readonly format?: TextureFormat | undefined;
226
+ /** Texture allocation facts retained for capability/lifetime inspection. */
227
+ readonly dimension?: GPUTextureDimension | undefined;
228
+ readonly extent?:
229
+ | { readonly width: number; readonly height: number; readonly depthOrArrayLayers: number }
230
+ | undefined;
218
231
  }[];
232
+ /** Monotonic graph generation assigned when this graph was compiled. */
233
+ readonly generation: number;
219
234
  }
220
235
 
221
236
  export interface RenderGraphFrame {
@@ -230,13 +245,36 @@ export interface RenderGraphPassExecution {
230
245
 
231
246
  export type RenderGraphPassRunner = (pass: RenderGraphPassExecution, encode: () => void) => void;
232
247
 
248
+ /**
249
+ * The graph-neutral boundary for observing and decorating an actual pass.
250
+ * Render owns the policy supplied here; the graph only preserves pass
251
+ * provenance and the existing encoder boundaries.
252
+ */
253
+ export interface RenderGraphPassInstrumentationScope {
254
+ readonly renderPassDescriptor?: (descriptor: RenderPassDescriptor) => RenderPassDescriptor;
255
+ readonly computePassDescriptor?: (descriptor: ComputePassDescriptor) => ComputePassDescriptor;
256
+ readonly beforeCopy?: (encoder: RhiCommandEncoder) => void;
257
+ readonly afterCopy?: (encoder: RhiCommandEncoder) => void;
258
+ }
259
+
260
+ export interface RenderGraphPassInstrumentation<FrameCtx extends RenderGraphFrame> {
261
+ begin(
262
+ pass: RenderGraphPassExecution,
263
+ frame: FrameCtx,
264
+ ): RenderGraphPassInstrumentationScope | undefined;
265
+ }
266
+
233
267
  export interface RenderGraphCompileOptions {
234
268
  readonly device: RhiDevice;
235
269
  readonly surfaceSize: { readonly width: number; readonly height: number };
236
270
  }
237
271
 
238
272
  export interface CompiledRenderGraph<FrameCtx extends RenderGraphFrame> {
239
- execute(frame: FrameCtx, runPass?: RenderGraphPassRunner): Result<void, RenderGraphError>;
273
+ execute(
274
+ frame: FrameCtx,
275
+ runPass?: RenderGraphPassRunner,
276
+ instrumentation?: RenderGraphPassInstrumentation<FrameCtx>,
277
+ ): Result<void, RenderGraphError>;
240
278
  inspect(): CompiledRenderGraphInfo;
241
279
  retire(): Promise<Result<void, RenderGraphError>>;
242
280
  }