• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 28667257246

03 Jul 2026 02:36PM UTC coverage: 71.982%. First build
28667257246

Pull #2720

github

web-flow
Merge b95bd7f7f into 5153dce30
Pull Request #2720: feat(experimental) Import fixed-width GPUVector chunks into command graphs

10806 of 16928 branches covered (63.84%)

Branch coverage included in aggregate %.

27 of 30 new or added lines in 1 file covered. (90.0%)

21195 of 27529 relevant lines covered (76.99%)

10895.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.32
/modules/experimental/src/gpu-primitives/gpu-command-graph.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {Buffer, Texture, textureFormatDecoder} from '@luma.gl/core';
6
import type {
7
  CommandEncoder,
8
  ComputePass,
9
  Device,
10
  Framebuffer,
11
  RenderPass,
12
  RenderPassProps,
13
  TextureFormat,
14
  TextureView
15
} from '@luma.gl/core';
16
import {DynamicBuffer, DynamicTexture} from '@luma.gl/engine';
17
import {
18
  GPUData,
19
  type GPUVector,
20
  type GPUVectorFormat,
21
  getGPUVectorFormatInfo,
22
  isValueListGPUVectorFormat,
23
  isVertexListGPUVectorFormat
24
} from '@luma.gl/tables';
25

26
/** GPU buffer use declared by one graph node. */
27
export type GraphBufferUsage =
28
  | 'storage-read'
29
  | 'storage-write'
30
  | 'storage-read-write'
31
  | 'uniform'
32
  | 'copy-source'
33
  | 'copy-destination'
34
  | 'indirect'
35
  | 'vertex'
36
  | 'index';
37

38
/** GPU texture use declared by one graph node. */
39
export type GraphTextureUsage =
40
  | 'sampled'
41
  | 'storage-read'
42
  | 'storage-write'
43
  | 'storage-read-write'
44
  | 'render-attachment'
45
  | 'copy-source'
46
  | 'copy-destination';
47

48
/** Buffer accepted as a fixed or per-encoding graph import. */
49
export type GraphImportedBuffer = Buffer | DynamicBuffer;
50

51
/** Texture accepted as a fixed or per-encoding graph import. */
52
export type GraphImportedTexture = Texture | DynamicTexture;
53

54
/** Descriptor for one imported or transient graph buffer. */
55
export type GraphBufferDescriptor = {
56
  id: string;
57
  byteLength: number;
58
  usage: number;
59
};
60

61
export type GraphTextureDimension = '1d' | '2d' | '2d-array' | 'cube' | 'cube-array' | '3d';
62
export type GraphTextureAspect = 'all' | 'stencil-only' | 'depth-only';
63

64
/** Descriptor for one fixed-size imported or transient graph texture. */
65
export type GraphTextureDescriptor<Format extends TextureFormat = TextureFormat> = {
66
  id: string;
67
  format: Format;
68
  width: number;
69
  height: number;
70
  usage: number;
71
  dimension?: GraphTextureDimension;
72
  depth?: number;
73
  mipLevels?: number;
74
  samples?: number;
75
};
76

77
type NormalizedGraphTextureDescriptor<Format extends TextureFormat = TextureFormat> = {
78
  id: string;
79
  format: Format;
80
  width: number;
81
  height: number;
82
  usage: number;
83
  dimension: GraphTextureDimension;
84
  depth: number;
85
  mipLevels: number;
86
  samples: number;
87
};
88

89
/** Opaque logical buffer tracked by a {@link GPUCommandGraph}. */
90
export class GraphBufferHandle {
91
  readonly id: string;
92
  readonly byteLength: number;
93
  readonly usage: number;
94
  readonly transient: boolean;
95
  /** @internal */
96
  readonly graph: GPUCommandGraph<any>;
97
  /** @internal */
98
  readonly defaultBuffer?: GraphImportedBuffer;
99

100
  /** @internal */
101
  constructor(
102
    graph: GPUCommandGraph<any>,
103
    descriptor: GraphBufferDescriptor,
104
    transient: boolean,
105
    defaultBuffer?: GraphImportedBuffer
106
  ) {
107
    this.graph = graph;
908✔
108
    this.id = descriptor.id;
908✔
109
    this.byteLength = descriptor.byteLength;
908✔
110
    this.usage = descriptor.usage;
908✔
111
    this.transient = transient;
908✔
112
    this.defaultBuffer = defaultBuffer;
908✔
113
  }
114
}
115

116
/** Typed logical range within one graph buffer. */
117
export class GraphDataView<T extends GPUVectorFormat = GPUVectorFormat> {
118
  readonly buffer: GraphBufferHandle;
119
  readonly format: T;
120
  readonly length: number;
121
  readonly byteOffset: number;
122
  readonly byteStride: number;
123
  readonly rowByteLength: number;
124

125
  /** @internal */
126
  constructor(
127
    buffer: GraphBufferHandle,
128
    props: {
129
      format: T;
130
      length: number;
131
      byteOffset: number;
132
      byteStride: number;
133
      rowByteLength: number;
134
    }
135
  ) {
136
    this.buffer = buffer;
906✔
137
    this.format = props.format;
906✔
138
    this.length = props.length;
906✔
139
    this.byteOffset = props.byteOffset;
906✔
140
    this.byteStride = props.byteStride;
906✔
141
    this.rowByteLength = props.rowByteLength;
906✔
142
  }
143
}
144

145
/** Ordered graph data views that preserve one fixed-width GPUVector's chunk boundaries. */
146
export class GraphVectorView<T extends GPUVectorFormat = GPUVectorFormat> {
147
  readonly id: string;
148
  readonly name: string;
149
  readonly format: T;
150
  readonly length: number;
151
  readonly valueLength: number;
152
  readonly stride: number;
153
  readonly byteStride: number;
154
  readonly rowByteLength: number;
155
  readonly data: readonly GraphDataView<T>[];
156

157
  /** @internal */
158
  constructor(props: {
159
    id: string;
160
    name: string;
161
    format: T;
162
    length: number;
163
    valueLength: number;
164
    stride: number;
165
    byteStride: number;
166
    rowByteLength: number;
167
    data: readonly GraphDataView<T>[];
168
  }) {
169
    this.id = props.id;
2✔
170
    this.name = props.name;
2✔
171
    this.format = props.format;
2✔
172
    this.length = props.length;
2✔
173
    this.valueLength = props.valueLength;
2✔
174
    this.stride = props.stride;
2✔
175
    this.byteStride = props.byteStride;
2✔
176
    this.rowByteLength = props.rowByteLength;
2✔
177
    this.data = props.data;
2✔
178
  }
179
}
180

181
/** Opaque logical texture tracked by a {@link GPUCommandGraph}. */
182
export class GraphTextureHandle<Format extends TextureFormat = TextureFormat> {
183
  readonly id: string;
184
  readonly format: Format;
185
  readonly width: number;
186
  readonly height: number;
187
  readonly usage: number;
188
  readonly dimension: GraphTextureDimension;
189
  readonly depth: number;
190
  readonly mipLevels: number;
191
  readonly samples: number;
192
  readonly transient: boolean;
193
  /** @internal */
194
  readonly graph: GPUCommandGraph<any>;
195
  /** @internal */
196
  readonly defaultTexture?: GraphImportedTexture;
197

198
  /** @internal */
199
  constructor(
200
    graph: GPUCommandGraph<any>,
201
    descriptor: NormalizedGraphTextureDescriptor<Format>,
202
    transient: boolean,
203
    defaultTexture?: GraphImportedTexture
204
  ) {
205
    this.graph = graph;
22✔
206
    this.id = descriptor.id;
22✔
207
    this.format = descriptor.format;
22✔
208
    this.width = descriptor.width;
22✔
209
    this.height = descriptor.height;
22✔
210
    this.usage = descriptor.usage;
22✔
211
    this.dimension = descriptor.dimension;
22✔
212
    this.depth = descriptor.depth;
22✔
213
    this.mipLevels = descriptor.mipLevels;
22✔
214
    this.samples = descriptor.samples;
22✔
215
    this.transient = transient;
22✔
216
    this.defaultTexture = defaultTexture;
22✔
217
  }
218
}
219

220
export type GraphTextureViewProps = {
221
  dimension?: GraphTextureDimension;
222
  aspect?: GraphTextureAspect;
223
  baseMipLevel?: number;
224
  mipLevelCount?: number;
225
  baseArrayLayer?: number;
226
  arrayLayerCount?: number;
227
};
228

229
/** Logical mip, layer, and aspect range within one graph texture. */
230
export class GraphTextureView<Format extends TextureFormat = TextureFormat> {
231
  readonly texture: GraphTextureHandle<Format>;
232
  readonly format: Format;
233
  readonly dimension: GraphTextureDimension;
234
  readonly aspect: GraphTextureAspect;
235
  readonly baseMipLevel: number;
236
  readonly mipLevelCount: number;
237
  readonly baseArrayLayer: number;
238
  readonly arrayLayerCount: number;
239
  readonly width: number;
240
  readonly height: number;
241
  readonly depth: number;
242

243
  /** @internal */
244
  constructor(
245
    texture: GraphTextureHandle<Format>,
246
    props: Required<GraphTextureViewProps> & {width: number; height: number; depth: number}
247
  ) {
248
    this.texture = texture;
20✔
249
    this.format = texture.format;
20✔
250
    this.dimension = props.dimension;
20✔
251
    this.aspect = props.aspect;
20✔
252
    this.baseMipLevel = props.baseMipLevel;
20✔
253
    this.mipLevelCount = props.mipLevelCount;
20✔
254
    this.baseArrayLayer = props.baseArrayLayer;
20✔
255
    this.arrayLayerCount = props.arrayLayerCount;
20✔
256
    this.width = props.width;
20✔
257
    this.height = props.height;
20✔
258
    this.depth = props.depth;
20✔
259
  }
260
}
261

262
/** One buffer use declared by a graph node. */
263
export type GraphBufferUse = {
264
  buffer: GraphBufferHandle | GraphDataView;
265
  usage: GraphBufferUsage;
266
};
267

268
/** One texture or texture-view use declared by a graph node. */
269
export type GraphTextureUse = {
270
  texture: GraphTextureHandle | GraphTextureView;
271
  usage: GraphTextureUsage;
272
};
273

274
/** One resource use declared by a graph node. */
275
export type GraphResourceUse = GraphBufferUse | GraphTextureUse;
276

277
/** Graph-owned texture attachments resolved into a framebuffer for a render node. */
278
export type GraphRenderPassAttachments = {
279
  colorAttachments: GraphTextureView[];
280
  depthStencilAttachment?: GraphTextureView;
281
};
282

283
/** Context available while compiling one graph node. */
284
export type GPUCommandGraphCompileContext = {
285
  device: Device;
286
};
287

288
/** Context shared by every executable graph node. */
289
export type GPUCommandGraphEncodeContext<Parameters> = {
290
  commandEncoder: CommandEncoder;
291
  parameters: Parameters;
292
  getBuffer: (buffer: GraphBufferHandle | GraphDataView) => Buffer;
293
  getTexture: (texture: GraphTextureHandle | GraphTextureView) => Texture;
294
  getTextureView: (texture: GraphTextureHandle | GraphTextureView) => TextureView;
295
};
296

297
/** Compiled compute-pass callback. */
298
export type GPUCommandGraphComputeExecutable<Parameters> = {
299
  encode: (context: GPUCommandGraphEncodeContext<Parameters> & {computePass: ComputePass}) => void;
300
  destroy?: () => void;
301
};
302

303
/** Compiled render-pass callback. */
304
export type GPUCommandGraphRenderExecutable<Parameters> = {
305
  getRenderPassProps?: (context: GPUCommandGraphEncodeContext<Parameters>) => RenderPassProps;
306
  encode: (context: GPUCommandGraphEncodeContext<Parameters> & {renderPass: RenderPass}) => void;
307
  destroy?: () => void;
308
};
309

310
/** Compiled command-encoder callback used for copies and other pass-independent commands. */
311
export type GPUCommandGraphCopyExecutable<Parameters> = {
312
  encode: (context: GPUCommandGraphEncodeContext<Parameters>) => void;
313
  destroy?: () => void;
314
};
315

316
type GPUCommandGraphNodeBase = {
317
  id: string;
318
  resources?: GraphResourceUse[];
319
  dependsOn?: string[];
320
};
321

322
export type GPUCommandGraphComputeNode<Parameters> = GPUCommandGraphNodeBase & {
323
  type: 'compute';
324
  compile: (context: GPUCommandGraphCompileContext) => GPUCommandGraphComputeExecutable<Parameters>;
325
};
326

327
export type GPUCommandGraphRenderNode<Parameters> = GPUCommandGraphNodeBase & {
328
  type: 'render';
329
  attachments?: GraphRenderPassAttachments;
330
  compile: (context: GPUCommandGraphCompileContext) => GPUCommandGraphRenderExecutable<Parameters>;
331
};
332

333
export type GPUCommandGraphCopyNode<Parameters> = GPUCommandGraphNodeBase & {
334
  type: 'copy';
335
  compile: (context: GPUCommandGraphCompileContext) => GPUCommandGraphCopyExecutable<Parameters>;
336
};
337

338
export type GPUCommandGraphNode<Parameters> =
339
  | GPUCommandGraphComputeNode<Parameters>
340
  | GPUCommandGraphRenderNode<Parameters>
341
  | GPUCommandGraphCopyNode<Parameters>;
342

343
/** Resource-allocation and scheduling statistics for one compiled graph. */
344
export type GPUCommandGraphStats = {
345
  nodeOrder: string[];
346
  logicalTransientBufferCount: number;
347
  physicalTransientBufferCount: number;
348
  logicalTransientBytes: number;
349
  physicalTransientBytes: number;
350
  reusedTransientBytes: number;
351
  reusePercentage: number;
352
  logicalTransientTextureCount: number;
353
  physicalTransientTextureCount: number;
354
  logicalTransientTextureBytes: number;
355
  physicalTransientTextureBytes: number;
356
  reusedTransientTextureBytes: number;
357
  textureReusePercentage: number;
358
};
359

360
/** Options supplied while encoding one compiled graph. */
361
export type GPUCommandGraphEncodeOptions<Parameters> = {
362
  parameters: Parameters;
363
  buffers?: Record<string, GraphImportedBuffer>;
364
  textures?: Record<string, GraphImportedTexture>;
365
};
366

367
type CompiledNode<Parameters> = {
368
  node: GPUCommandGraphNode<Parameters>;
369
  executable:
370
    | GPUCommandGraphComputeExecutable<Parameters>
371
    | GPUCommandGraphRenderExecutable<Parameters>
372
    | GPUCommandGraphCopyExecutable<Parameters>;
373
};
374

375
type BufferTransientAllocation = {
376
  byteLength: number;
377
  usage: number;
378
  lastUse: number;
379
  handles: GraphBufferHandle[];
380
  buffer?: Buffer;
381
};
382

383
type TextureTransientAllocation = {
384
  descriptor: NormalizedGraphTextureDescriptor;
385
  byteLength: number;
386
  lastUse: number;
387
  handles: GraphTextureHandle[];
388
  texture?: Texture;
389
};
390

391
type CachedTextureView = {
392
  logicalView: GraphTextureView;
393
  texture: Texture;
394
  view: TextureView;
395
};
396

397
type CachedFramebuffer = {
398
  nodeId: string;
399
  colorAttachments: TextureView[];
400
  depthStencilAttachment?: TextureView;
401
  framebuffer: Framebuffer;
402
};
403

404
/**
405
 * Declarative WebGPU command graph with explicit resource access and ownership.
406
 *
407
 * The graph compiles resource hazards, transient lifetimes, and node resources,
408
 * but encoding and submission remain controlled by the application.
409
 */
410
export class GPUCommandGraph<Parameters = void> {
411
  readonly device: Device;
412
  readonly id: string;
413

414
  private readonly buffers = new Map<string, GraphBufferHandle>();
146✔
415
  private readonly textures = new Map<string, GraphTextureHandle>();
146✔
416
  private readonly tableBufferHandles = new Map<Buffer, GraphBufferHandle>();
146✔
417
  private readonly nodes: GPUCommandGraphNode<Parameters>[] = [];
146✔
418
  private readonly nodeIds = new Set<string>();
146✔
419
  private compiled = false;
146✔
420

421
  constructor(device: Device, props: {id?: string} = {}) {
72✔
422
    if (device.type !== 'webgpu') {
146!
423
      throw new Error('GPUCommandGraph requires a WebGPU device');
×
424
    }
425
    this.device = device;
146✔
426
    this.id = props.id ?? 'gpu-command-graph';
146✔
427
  }
428

429
  /** Declares a caller-owned buffer that can be supplied now or for each encoding. */
430
  importBuffer(
431
    descriptor: GraphBufferDescriptor,
432
    defaultBuffer?: GraphImportedBuffer
433
  ): GraphBufferHandle {
434
    this.assertMutable();
266✔
435
    validateGraphBufferDescriptor(descriptor);
266✔
436
    if (defaultBuffer) {
266✔
437
      validateImportedBuffer(defaultBuffer, descriptor, this.device);
264✔
438
    }
439
    return this.addBuffer(new GraphBufferHandle(this, descriptor, false, defaultBuffer));
264✔
440
  }
441

442
  /** Declares one graph-owned scratch buffer. */
443
  createTransientBuffer(descriptor: GraphBufferDescriptor): GraphBufferHandle {
444
    this.assertMutable();
644✔
445
    validateGraphBufferDescriptor(descriptor);
644✔
446
    return this.addBuffer(new GraphBufferHandle(this, descriptor, true));
644✔
447
  }
448

449
  /** Creates one typed range over a graph buffer. */
450
  createDataView<T extends GPUVectorFormat>(
451
    buffer: GraphBufferHandle,
452
    props: {
453
      format: T;
454
      length: number;
455
      byteOffset?: number;
456
      byteStride?: number;
457
      rowByteLength?: number;
458
    }
459
  ): GraphDataView<T> {
460
    this.assertBuffer(buffer);
908✔
461
    const formatInfo = getGPUVectorFormatInfo(props.format);
908✔
462
    const byteOffset = props.byteOffset ?? 0;
908✔
463
    const rowByteLength = props.rowByteLength ?? formatInfo.byteLength;
908✔
464
    const byteStride = props.byteStride ?? rowByteLength;
908✔
465
    validateGraphDataView(buffer, {
908✔
466
      length: props.length,
467
      byteOffset,
468
      byteStride,
469
      rowByteLength
470
    });
471
    return new GraphDataView(buffer, {
906✔
472
      format: props.format,
473
      length: props.length,
474
      byteOffset,
475
      byteStride,
476
      rowByteLength
477
    });
478
  }
479

480
  /** Imports one borrowed GPUData range and returns its typed graph view. */
481
  importGPUData<T extends GPUVectorFormat>(id: string, data: GPUData<T>): GraphDataView<T> {
482
    return this.importGPUDataView(id, data);
4✔
483
  }
484

485
  /** Imports all chunks of one fixed-width GPUVector without packing them. */
486
  importGPUVector<T extends GPUVectorFormat>(id: string, vector: GPUVector<T>): GraphVectorView<T> {
487
    if (vector.bufferLayout) {
6✔
488
      throw new Error(`GPUCommandGraph import "${id}" does not accept interleaved GPUVector data`);
2✔
489
    }
490
    const format = vector.format ?? vector.data[0]?.format;
4!
491
    if (!format) {
4!
NEW
492
      throw new Error(`GPUCommandGraph import "${id}" requires GPUVector.format`);
×
493
    }
494
    if (isVertexListGPUVectorFormat(format) || isValueListGPUVectorFormat(format)) {
4✔
495
      throw new Error(`GPUCommandGraph import "${id}" requires a fixed-width GPUVector format`);
2✔
496
    }
497
    const data = vector.data.map((chunk, chunkIndex) => {
2✔
498
      if (chunk.format !== format) {
6!
NEW
499
        throw new Error(`GPUCommandGraph import "${id}" requires matching GPUVector chunk formats`);
×
500
      }
501
      const chunkId = vector.data.length === 1 ? id : `${id}-chunk-${chunkIndex}`;
6!
502
      return this.importGPUDataView(chunkId, chunk);
6✔
503
    });
504
    return new GraphVectorView({
2✔
505
      id,
506
      name: vector.name,
507
      format,
508
      length: vector.length,
509
      valueLength: vector.valueLength,
510
      stride: vector.stride,
511
      byteStride: vector.byteStride,
512
      rowByteLength: vector.rowByteLength,
513
      data
514
    });
515
  }
516

517
  /** Declares a caller-owned fixed-size texture that can be supplied now or while encoding. */
518
  importTexture<Format extends TextureFormat>(
519
    descriptor: GraphTextureDescriptor<Format>,
520
    defaultTexture?: GraphImportedTexture
521
  ): GraphTextureHandle<Format> {
522
    this.assertMutable();
6✔
523
    const normalizedDescriptor = normalizeGraphTextureDescriptor(descriptor, this.device);
6✔
524
    if (defaultTexture) {
6!
525
      validateImportedTexture(defaultTexture, normalizedDescriptor, this.device);
6✔
526
    }
527
    return this.addTexture(
4✔
528
      new GraphTextureHandle(this, normalizedDescriptor, false, defaultTexture)
529
    );
530
  }
531

532
  /** Declares one graph-owned fixed-size transient texture. */
533
  createTransientTexture<Format extends TextureFormat>(
534
    descriptor: GraphTextureDescriptor<Format>
535
  ): GraphTextureHandle<Format> {
536
    this.assertMutable();
20✔
537
    const normalizedDescriptor = normalizeGraphTextureDescriptor(descriptor, this.device);
20✔
538
    return this.addTexture(new GraphTextureHandle(this, normalizedDescriptor, true));
18✔
539
  }
540

541
  /** Creates one logical mip, layer, and aspect range over a graph texture. */
542
  createTextureView<Format extends TextureFormat>(
543
    texture: GraphTextureHandle<Format>,
544
    props: GraphTextureViewProps = {}
12✔
545
  ): GraphTextureView<Format> {
546
    this.assertTexture(texture);
22✔
547
    const normalizedProps = normalizeGraphTextureView(texture, props);
22✔
548
    return new GraphTextureView(texture, normalizedProps);
20✔
549
  }
550

551
  addComputePass(node: Omit<GPUCommandGraphComputeNode<Parameters>, 'type'>): void {
552
    this.addNode({...node, type: 'compute'});
900✔
553
  }
554

555
  addRenderPass(node: Omit<GPUCommandGraphRenderNode<Parameters>, 'type'>): void {
556
    if (node.attachments) {
4!
557
      this.validateRenderAttachments(node.id, node.attachments);
4✔
558
    }
559
    const attachmentUses: GraphTextureUse[] = node.attachments
4!
560
      ? [
561
          ...node.attachments.colorAttachments.map(texture => ({
6✔
562
            texture,
563
            usage: 'render-attachment' as const
564
          })),
565
          ...(node.attachments.depthStencilAttachment
4✔
566
            ? [
567
                {
568
                  texture: node.attachments.depthStencilAttachment,
569
                  usage: 'render-attachment' as const
570
                }
571
              ]
572
            : [])
573
        ]
574
      : [];
575
    this.addNode({
4✔
576
      ...node,
577
      resources: [...(node.resources ?? []), ...attachmentUses],
6✔
578
      type: 'render'
579
    });
580
  }
581

582
  addCopyPass(node: Omit<GPUCommandGraphCopyNode<Parameters>, 'type'>): void {
583
    this.addNode({...node, type: 'copy'});
12✔
584
  }
585

586
  /** Compiles scheduling, transient allocations, and executable node resources. */
587
  compile(): CompiledGPUCommandGraph<Parameters> {
588
    this.assertMutable();
124✔
589
    this.compiled = true;
124✔
590
    const nodeOrder = getNodeOrder(this.nodes);
124✔
591
    const bufferPlan = getBufferTransientAllocationPlan(nodeOrder, this.buffers.values());
120✔
592
    const texturePlan = getTextureTransientAllocationPlan(nodeOrder, this.textures.values());
120✔
593
    const transientBuffers = new Map<GraphBufferHandle, Buffer>();
120✔
594
    const transientTextures = new Map<GraphTextureHandle, Texture>();
120✔
595

596
    for (const allocation of bufferPlan) {
120✔
597
      allocation.buffer = this.device.createBuffer({
108✔
598
        id: `${this.id}-transient-buffer-${bufferPlan.indexOf(allocation)}`,
599
        byteLength: allocation.byteLength,
600
        usage: allocation.usage
601
      });
602
      for (const handle of allocation.handles) {
108✔
603
        transientBuffers.set(handle, allocation.buffer);
606✔
604
      }
605
    }
606
    for (const allocation of texturePlan) {
120✔
607
      allocation.texture = this.device.createTexture({
12✔
608
        ...allocation.descriptor,
609
        id: `${this.id}-transient-texture-${texturePlan.indexOf(allocation)}`
610
      });
611
      for (const handle of allocation.handles) {
12✔
612
        transientTextures.set(handle, allocation.texture);
14✔
613
      }
614
    }
615

616
    const compiledNodes: CompiledNode<Parameters>[] = [];
120✔
617
    try {
120✔
618
      for (const node of nodeOrder) {
120✔
619
        compiledNodes.push({node, executable: node.compile({device: this.device})});
904✔
620
      }
621
    } catch (error) {
622
      for (const compiledNode of compiledNodes) {
×
623
        compiledNode.executable.destroy?.();
×
624
      }
625
      for (const allocation of bufferPlan) {
×
626
        allocation.buffer?.destroy();
×
627
      }
628
      for (const allocation of texturePlan) {
×
629
        allocation.texture?.destroy();
×
630
      }
631
      throw error;
×
632
    }
633

634
    const logicalTransientBytes = Array.from(this.buffers.values())
120✔
635
      .filter(buffer => buffer.transient)
870✔
636
      .reduce((sum, buffer) => sum + buffer.byteLength, 0);
606✔
637
    const physicalTransientBytes = bufferPlan.reduce(
120✔
638
      (sum, allocation) => sum + allocation.byteLength,
108✔
639
      0
640
    );
641
    const reusedTransientBytes = Math.max(0, logicalTransientBytes - physicalTransientBytes);
120✔
642
    const logicalTransientTextureBytes = Array.from(this.textures.values())
120✔
643
      .filter(texture => texture.transient)
18✔
644
      .reduce((sum, texture) => sum + getTextureByteLength(texture), 0);
14✔
645
    const physicalTransientTextureBytes = texturePlan.reduce(
120✔
646
      (sum, allocation) => sum + allocation.byteLength,
12✔
647
      0
648
    );
649
    const reusedTransientTextureBytes = Math.max(
120✔
650
      0,
651
      logicalTransientTextureBytes - physicalTransientTextureBytes
652
    );
653
    const stats: GPUCommandGraphStats = {
120✔
654
      nodeOrder: nodeOrder.map(node => node.id),
904✔
655
      logicalTransientBufferCount: Array.from(this.buffers.values()).filter(
656
        buffer => buffer.transient
870✔
657
      ).length,
658
      physicalTransientBufferCount: bufferPlan.length,
659
      logicalTransientBytes,
660
      physicalTransientBytes,
661
      reusedTransientBytes,
662
      reusePercentage:
663
        logicalTransientBytes > 0 ? (reusedTransientBytes / logicalTransientBytes) * 100 : 0,
120✔
664
      logicalTransientTextureCount: Array.from(this.textures.values()).filter(
665
        texture => texture.transient
18✔
666
      ).length,
667
      physicalTransientTextureCount: texturePlan.length,
668
      logicalTransientTextureBytes,
669
      physicalTransientTextureBytes,
670
      reusedTransientTextureBytes,
671
      textureReusePercentage:
672
        logicalTransientTextureBytes > 0
120✔
673
          ? (reusedTransientTextureBytes / logicalTransientTextureBytes) * 100
674
          : 0
675
    };
676

677
    return new CompiledGPUCommandGraph({
120✔
678
      device: this.device,
679
      id: this.id,
680
      buffers: new Map(this.buffers),
681
      textures: new Map(this.textures),
682
      compiledNodes,
683
      transientBuffers,
684
      transientTextures,
685
      bufferTransientAllocations: bufferPlan,
686
      textureTransientAllocations: texturePlan,
687
      stats
688
    });
689
  }
690

691
  private addNode(node: GPUCommandGraphNode<Parameters>): void {
692
    this.assertMutable();
916✔
693
    if (!node.id) {
916!
694
      throw new Error('GPUCommandGraph node id is required');
×
695
    }
696
    if (this.nodeIds.has(node.id)) {
916!
697
      throw new Error(`GPUCommandGraph node id "${node.id}" is already in use`);
×
698
    }
699
    for (const resource of node.resources ?? []) {
916✔
700
      if (isGraphBufferUse(resource)) {
2,502✔
701
        const buffer = getBufferHandle(resource.buffer);
2,468✔
702
        this.assertBuffer(buffer);
2,468✔
703
        validateBufferUseAgainstDescriptor(buffer, resource.usage);
2,468✔
704
      } else {
705
        const texture = getTextureHandle(resource.texture);
34✔
706
        this.assertTexture(texture);
34✔
707
        validateTextureUseAgainstDescriptor(texture, resource.usage);
34✔
708
        validateTextureViewForUsage(resource.texture, resource.usage);
32✔
709
      }
710
    }
711
    this.nodeIds.add(node.id);
912✔
712
    this.nodes.push(node);
912✔
713
  }
714

715
  private addBuffer(buffer: GraphBufferHandle): GraphBufferHandle {
716
    if (this.buffers.has(buffer.id) || this.textures.has(buffer.id)) {
908!
717
      throw new Error(`GPUCommandGraph resource id "${buffer.id}" is already in use`);
×
718
    }
719
    this.buffers.set(buffer.id, buffer);
908✔
720
    return buffer;
908✔
721
  }
722

723
  private addTexture<Format extends TextureFormat>(
724
    texture: GraphTextureHandle<Format>
725
  ): GraphTextureHandle<Format> {
726
    if (this.buffers.has(texture.id) || this.textures.has(texture.id)) {
22!
727
      throw new Error(`GPUCommandGraph resource id "${texture.id}" is already in use`);
×
728
    }
729
    this.textures.set(texture.id, texture);
22✔
730
    return texture;
22✔
731
  }
732

733
  private importGPUDataView<T extends GPUVectorFormat>(
734
    id: string,
735
    data: GPUData<T>
736
  ): GraphDataView<T> {
737
    if (!data.format) {
10!
NEW
738
      throw new Error(`GPUCommandGraph import "${id}" requires GPUData.format`);
×
739
    }
740
    const coreBuffer = getCoreBuffer(data.buffer);
10✔
741
    let handle = this.tableBufferHandles.get(coreBuffer);
10✔
742
    if (!handle) {
10✔
743
      handle = this.importBuffer(
6✔
744
        {id, byteLength: coreBuffer.byteLength, usage: coreBuffer.usage},
745
        data.buffer
746
      );
747
      this.tableBufferHandles.set(coreBuffer, handle);
6✔
748
    }
749
    return this.createDataView(handle, {
10✔
750
      format: data.format,
751
      length: data.length,
752
      byteOffset: data.byteOffset,
753
      byteStride: data.byteStride,
754
      rowByteLength: data.rowByteLength
755
    });
756
  }
757

758
  private assertBuffer(buffer: GraphBufferHandle): void {
759
    if (buffer.graph !== this || this.buffers.get(buffer.id) !== buffer) {
3,376!
760
      throw new Error(`Graph buffer "${buffer.id}" does not belong to ${this.id}`);
×
761
    }
762
  }
763

764
  private assertTexture(texture: GraphTextureHandle): void {
765
    if (texture.graph !== this || this.textures.get(texture.id) !== texture) {
64!
766
      throw new Error(`Graph texture "${texture.id}" does not belong to ${this.id}`);
×
767
    }
768
  }
769

770
  private assertMutable(): void {
771
    if (this.compiled) {
1,976!
772
      throw new Error(`GPUCommandGraph "${this.id}" has already been compiled`);
×
773
    }
774
  }
775

776
  private validateRenderAttachments(id: string, attachments: GraphRenderPassAttachments): void {
777
    if (attachments.colorAttachments.length === 0 && !attachments.depthStencilAttachment) {
4!
778
      throw new Error(`GPUCommandGraph render node "${id}" requires at least one attachment`);
×
779
    }
780
    const allAttachments = [
4✔
781
      ...attachments.colorAttachments,
782
      ...(attachments.depthStencilAttachment ? [attachments.depthStencilAttachment] : [])
4✔
783
    ];
784
    for (const attachment of allAttachments) {
4✔
785
      this.assertTexture(attachment.texture);
8✔
786
      if (
8!
787
        attachment.dimension !== '2d' ||
24✔
788
        attachment.mipLevelCount !== 1 ||
789
        attachment.arrayLayerCount !== 1
790
      ) {
791
        throw new Error(
×
792
          `GPUCommandGraph render node "${id}" attachments must be single-mip, single-layer 2d views`
793
        );
794
      }
795
    }
796
    const [first, ...remaining] = allAttachments;
4✔
797
    for (const attachment of remaining) {
4✔
798
      if (
4!
799
        attachment.width !== first.width ||
12✔
800
        attachment.height !== first.height ||
801
        attachment.texture.samples !== first.texture.samples
802
      ) {
803
        throw new Error(
×
804
          `GPUCommandGraph render node "${id}" attachments must have matching extent and samples`
805
        );
806
      }
807
    }
808
  }
809
}
810

811
/** Executable, fixed-capacity command graph. */
812
export class CompiledGPUCommandGraph<Parameters = void> {
813
  readonly device: Device;
814
  readonly id: string;
815
  readonly stats: GPUCommandGraphStats;
816

817
  private readonly buffers: Map<string, GraphBufferHandle>;
818
  private readonly textures: Map<string, GraphTextureHandle>;
819
  private readonly compiledNodes: CompiledNode<Parameters>[];
820
  private readonly transientBuffers: Map<GraphBufferHandle, Buffer>;
821
  private readonly transientTextures: Map<GraphTextureHandle, Texture>;
822
  private readonly bufferTransientAllocations: BufferTransientAllocation[];
823
  private readonly textureTransientAllocations: TextureTransientAllocation[];
824
  private readonly cachedTextureViews: CachedTextureView[] = [];
120✔
825
  private readonly cachedFramebuffers: CachedFramebuffer[] = [];
120✔
826
  private destroyed = false;
120✔
827

828
  /** @internal */
829
  constructor(props: {
830
    device: Device;
831
    id: string;
832
    buffers: Map<string, GraphBufferHandle>;
833
    textures: Map<string, GraphTextureHandle>;
834
    compiledNodes: CompiledNode<Parameters>[];
835
    transientBuffers: Map<GraphBufferHandle, Buffer>;
836
    transientTextures: Map<GraphTextureHandle, Texture>;
837
    bufferTransientAllocations: BufferTransientAllocation[];
838
    textureTransientAllocations: TextureTransientAllocation[];
839
    stats: GPUCommandGraphStats;
840
  }) {
841
    this.device = props.device;
120✔
842
    this.id = props.id;
120✔
843
    this.buffers = props.buffers;
120✔
844
    this.textures = props.textures;
120✔
845
    this.compiledNodes = props.compiledNodes;
120✔
846
    this.transientBuffers = props.transientBuffers;
120✔
847
    this.transientTextures = props.transientTextures;
120✔
848
    this.bufferTransientAllocations = props.bufferTransientAllocations;
120✔
849
    this.textureTransientAllocations = props.textureTransientAllocations;
120✔
850
    this.stats = props.stats;
120✔
851
  }
852

853
  /** Records every graph node into a caller-owned command encoder. */
854
  encode(commandEncoder: CommandEncoder, options: GPUCommandGraphEncodeOptions<Parameters>): void {
855
    if (this.destroyed) {
136!
856
      throw new Error(`CompiledGPUCommandGraph "${this.id}" has been destroyed`);
×
857
    }
858
    if (commandEncoder.device !== this.device) {
136!
859
      throw new Error('GPUCommandGraph command encoder must belong to the graph device');
×
860
    }
861

862
    const importedBuffers = this.resolveImportedBuffers(options.buffers ?? {});
136✔
863
    const importedTextures = this.resolveImportedTextures(options.textures ?? {});
132✔
864
    const getBuffer = (bufferOrView: GraphBufferHandle | GraphDataView): Buffer => {
130✔
865
      const handle = getBufferHandle(bufferOrView);
2,490✔
866
      const buffer = handle.transient
2,490✔
867
        ? this.transientBuffers.get(handle)
868
        : importedBuffers.get(handle);
869
      if (!buffer) {
2,490!
870
        throw new Error(`GPUCommandGraph buffer "${handle.id}" is not bound`);
×
871
      }
872
      return buffer;
2,490✔
873
    };
874
    const getTexture = (textureOrView: GraphTextureHandle | GraphTextureView): Texture => {
130✔
875
      const handle = getTextureHandle(textureOrView);
42✔
876
      const texture = handle.transient
42✔
877
        ? this.transientTextures.get(handle)
878
        : importedTextures.get(handle);
879
      if (!texture) {
42!
880
        throw new Error(`GPUCommandGraph texture "${handle.id}" is not bound`);
×
881
      }
882
      return texture;
42✔
883
    };
884
    const getTextureView = (textureOrView: GraphTextureHandle | GraphTextureView): TextureView => {
130✔
885
      const texture = getTexture(textureOrView);
36✔
886
      if (textureOrView instanceof GraphTextureHandle || isDefaultGraphTextureView(textureOrView)) {
36!
887
        return texture.view;
36✔
888
      }
889
      const cached = this.cachedTextureViews.find(
×
890
        entry => entry.logicalView === textureOrView && entry.texture === texture
×
891
      );
892
      if (cached) {
×
893
        return cached.view;
×
894
      }
895
      const view = texture.createView({
×
896
        format: textureOrView.format,
897
        dimension: textureOrView.dimension,
898
        aspect: textureOrView.aspect,
899
        baseMipLevel: textureOrView.baseMipLevel,
900
        mipLevelCount: textureOrView.mipLevelCount,
901
        baseArrayLayer: textureOrView.baseArrayLayer,
902
        arrayLayerCount: textureOrView.arrayLayerCount
903
      });
904
      this.cachedTextureViews.push({logicalView: textureOrView, texture, view});
×
905
      return view;
×
906
    };
907

908
    const baseContext: GPUCommandGraphEncodeContext<Parameters> = {
130✔
909
      commandEncoder,
910
      parameters: options.parameters,
911
      getBuffer,
912
      getTexture,
913
      getTextureView
914
    };
915

916
    for (const {node, executable} of this.compiledNodes) {
130✔
917
      switch (node.type) {
914✔
918
        case 'compute': {
919
          const computePass = commandEncoder.beginComputePass({id: node.id});
896✔
920
          computePass.pushDebugGroup(node.id);
896✔
921
          try {
896✔
922
            (executable as GPUCommandGraphComputeExecutable<Parameters>).encode({
896✔
923
              ...baseContext,
924
              computePass
925
            });
926
          } finally {
927
            computePass.popDebugGroup();
896✔
928
            computePass.end();
896✔
929
          }
930
          break;
896✔
931
        }
932
        case 'render': {
933
          const renderExecutable = executable as GPUCommandGraphRenderExecutable<Parameters>;
10✔
934
          const renderPassProps = renderExecutable.getRenderPassProps?.(baseContext) ?? {
10✔
935
            id: node.id
936
          };
937
          if (node.attachments && renderPassProps.framebuffer !== undefined) {
10!
938
            throw new Error(
×
939
              `GPUCommandGraph render node "${node.id}" cannot supply framebuffer with graph attachments`
940
            );
941
          }
942
          const framebuffer = node.attachments
10!
943
            ? this.getFramebuffer(node.id, node.attachments, getTextureView)
944
            : undefined;
945
          const renderPass = commandEncoder.beginRenderPass({
10✔
946
            ...renderPassProps,
947
            ...(framebuffer ? {framebuffer} : {})
10!
948
          });
949
          renderPass.pushDebugGroup(node.id);
10✔
950
          try {
10✔
951
            renderExecutable.encode({...baseContext, renderPass});
10✔
952
          } finally {
953
            renderPass.popDebugGroup();
10✔
954
            renderPass.end();
10✔
955
          }
956
          break;
10✔
957
        }
958
        case 'copy':
959
          (executable as GPUCommandGraphCopyExecutable<Parameters>).encode(baseContext);
8✔
960
          break;
6✔
961
      }
962
    }
963
  }
964

965
  /** Releases compiled node resources and graph-owned transient resources. */
966
  destroy(): void {
967
    if (this.destroyed) {
120!
968
      return;
×
969
    }
970
    for (const {executable} of this.compiledNodes) {
120✔
971
      executable.destroy?.();
904✔
972
    }
973
    for (const cached of this.cachedFramebuffers) {
120✔
974
      cached.framebuffer.destroy();
4✔
975
    }
976
    for (const cached of this.cachedTextureViews) {
120✔
977
      cached.view.destroy();
×
978
    }
979
    for (const allocation of this.bufferTransientAllocations) {
120✔
980
      allocation.buffer?.destroy();
108✔
981
    }
982
    for (const allocation of this.textureTransientAllocations) {
120✔
983
      allocation.texture?.destroy();
12✔
984
    }
985
    this.destroyed = true;
120✔
986
  }
987

988
  private resolveImportedBuffers(
989
    overrides: Record<string, GraphImportedBuffer>
990
  ): Map<GraphBufferHandle, Buffer> {
991
    const resolved = new Map<GraphBufferHandle, Buffer>();
136✔
992
    for (const [id, handle] of this.buffers) {
136✔
993
      if (handle.transient) {
894✔
994
        continue;
600✔
995
      }
996
      const importedBuffer = overrides[id] ?? handle.defaultBuffer;
294✔
997
      if (!importedBuffer) {
294✔
998
        throw new Error(`GPUCommandGraph imported buffer "${id}" is required`);
2✔
999
      }
1000
      validateImportedBuffer(importedBuffer, handle, this.device);
292✔
1001
      resolved.set(handle, getCoreBuffer(importedBuffer));
290✔
1002
    }
1003
    for (const id of Object.keys(overrides)) {
132✔
1004
      const handle = this.buffers.get(id);
4✔
1005
      if (!handle || handle.transient) {
4!
1006
        throw new Error(`GPUCommandGraph has no imported buffer named "${id}"`);
×
1007
      }
1008
    }
1009
    return resolved;
132✔
1010
  }
1011

1012
  private resolveImportedTextures(
1013
    overrides: Record<string, GraphImportedTexture>
1014
  ): Map<GraphTextureHandle, Texture> {
1015
    const resolved = new Map<GraphTextureHandle, Texture>();
132✔
1016
    for (const [id, handle] of this.textures) {
132✔
1017
      if (handle.transient) {
36✔
1018
        continue;
26✔
1019
      }
1020
      const importedTexture = overrides[id] ?? handle.defaultTexture;
10✔
1021
      if (!importedTexture) {
10!
1022
        throw new Error(`GPUCommandGraph imported texture "${id}" is required`);
×
1023
      }
1024
      validateImportedTexture(importedTexture, handle, this.device);
10✔
1025
      resolved.set(handle, getCoreTexture(importedTexture));
8✔
1026
    }
1027
    for (const id of Object.keys(overrides)) {
130✔
1028
      const handle = this.textures.get(id);
2✔
1029
      if (!handle || handle.transient) {
2!
1030
        throw new Error(`GPUCommandGraph has no imported texture named "${id}"`);
×
1031
      }
1032
    }
1033
    return resolved;
130✔
1034
  }
1035

1036
  private getFramebuffer(
1037
    nodeId: string,
1038
    attachments: GraphRenderPassAttachments,
1039
    getTextureView: (texture: GraphTextureView) => TextureView
1040
  ): Framebuffer {
1041
    const colorAttachments = attachments.colorAttachments.map(getTextureView);
10✔
1042
    const depthStencilAttachment = attachments.depthStencilAttachment
10✔
1043
      ? getTextureView(attachments.depthStencilAttachment)
1044
      : undefined;
1045
    const cached = this.cachedFramebuffers.find(
10✔
1046
      entry =>
1047
        entry.nodeId === nodeId &&
6✔
1048
        entry.depthStencilAttachment === depthStencilAttachment &&
1049
        entry.colorAttachments.length === colorAttachments.length &&
1050
        entry.colorAttachments.every((view, index) => view === colorAttachments[index])
12✔
1051
    );
1052
    if (cached) {
10✔
1053
      return cached.framebuffer;
6✔
1054
    }
1055
    const firstLogicalAttachment =
1056
      attachments.colorAttachments[0] ?? attachments.depthStencilAttachment!;
4!
1057
    const framebuffer = this.device.createFramebuffer({
4✔
1058
      id: `${this.id}-${nodeId}-framebuffer-${this.cachedFramebuffers.length}`,
1059
      width: firstLogicalAttachment.width,
1060
      height: firstLogicalAttachment.height,
1061
      colorAttachments,
1062
      depthStencilAttachment: depthStencilAttachment ?? null
6✔
1063
    });
1064
    this.cachedFramebuffers.push({
4✔
1065
      nodeId,
1066
      colorAttachments,
1067
      depthStencilAttachment,
1068
      framebuffer
1069
    });
1070
    return framebuffer;
4✔
1071
  }
1072
}
1073

1074
function getBufferHandle(buffer: GraphBufferHandle | GraphDataView): GraphBufferHandle {
1075
  return buffer instanceof GraphDataView ? buffer.buffer : buffer;
9,890✔
1076
}
1077

1078
function getTextureHandle(texture: GraphTextureHandle | GraphTextureView): GraphTextureHandle {
1079
  return texture instanceof GraphTextureView ? texture.texture : texture;
160✔
1080
}
1081

1082
function getCoreBuffer(buffer: GraphImportedBuffer): Buffer {
1083
  return buffer instanceof DynamicBuffer ? buffer.buffer : buffer;
856✔
1084
}
1085

1086
function getCoreTexture(texture: GraphImportedTexture): Texture {
1087
  if (texture instanceof DynamicTexture) {
24!
1088
    if (!texture.isReady) {
×
1089
      throw new Error(`GPUCommandGraph dynamic texture "${texture.id}" is not ready`);
×
1090
    }
1091
    return texture.texture;
×
1092
  }
1093
  return texture;
24✔
1094
}
1095

1096
function isGraphBufferUse(resource: GraphResourceUse): resource is GraphBufferUse {
1097
  return 'buffer' in resource;
9,988✔
1098
}
1099

1100
function validateGraphBufferDescriptor(descriptor: GraphBufferDescriptor): void {
1101
  if (!descriptor.id) {
910!
1102
    throw new Error('GPUCommandGraph buffer id is required');
×
1103
  }
1104
  if (!Number.isSafeInteger(descriptor.byteLength) || descriptor.byteLength < 0) {
910!
1105
    throw new Error(`GPUCommandGraph buffer "${descriptor.id}" requires a valid byteLength`);
×
1106
  }
1107
  if (!Number.isSafeInteger(descriptor.usage) || descriptor.usage <= 0) {
910!
1108
    throw new Error(`GPUCommandGraph buffer "${descriptor.id}" requires buffer usage flags`);
×
1109
  }
1110
}
1111

1112
function normalizeGraphTextureDescriptor<Format extends TextureFormat>(
1113
  descriptor: GraphTextureDescriptor<Format>,
1114
  device: Device
1115
): NormalizedGraphTextureDescriptor<Format> {
1116
  if (!descriptor.id) {
26!
1117
    throw new Error('GPUCommandGraph texture id is required');
×
1118
  }
1119
  const dimension = descriptor.dimension ?? '2d';
26✔
1120
  const depth = dimension === 'cube' ? 6 : (descriptor.depth ?? 1);
26!
1121
  const mipLevels = descriptor.mipLevels ?? 1;
26✔
1122
  const samples = descriptor.samples ?? 1;
26✔
1123
  for (const [name, value] of Object.entries({
26✔
1124
    width: descriptor.width,
1125
    height: descriptor.height,
1126
    depth,
1127
    mipLevels,
1128
    samples
1129
  })) {
1130
    if (!Number.isSafeInteger(value) || value <= 0) {
122✔
1131
      throw new Error(
2✔
1132
        `GPUCommandGraph texture "${descriptor.id}" ${name} must be a positive safe integer`
1133
      );
1134
    }
1135
  }
1136
  if (!Number.isSafeInteger(descriptor.usage) || descriptor.usage <= 0) {
24!
1137
    throw new Error(`GPUCommandGraph texture "${descriptor.id}" requires texture usage flags`);
×
1138
  }
1139
  if (!device.isTextureFormatSupported(descriptor.format)) {
24!
1140
    throw new Error(
×
1141
      `GPUCommandGraph texture "${descriptor.id}" format ${descriptor.format} is unsupported`
1142
    );
1143
  }
1144
  if (dimension === '1d' && (descriptor.height !== 1 || depth !== 1)) {
24!
1145
    throw new Error(`GPUCommandGraph 1d texture "${descriptor.id}" requires height and depth 1`);
×
1146
  }
1147
  if (dimension === 'cube' && descriptor.width !== descriptor.height) {
24!
1148
    throw new Error(`GPUCommandGraph cube texture "${descriptor.id}" must be square`);
×
1149
  }
1150
  if (dimension === 'cube-array' && (descriptor.width !== descriptor.height || depth % 6 !== 0)) {
24!
1151
    throw new Error(
×
1152
      `GPUCommandGraph cube-array texture "${descriptor.id}" must be square with depth divisible by 6`
1153
    );
1154
  }
1155
  if (mipLevels > device.getMipLevelCount(descriptor.width, descriptor.height, depth)) {
24!
1156
    throw new Error(`GPUCommandGraph texture "${descriptor.id}" declares too many mip levels`);
×
1157
  }
1158
  return {
24✔
1159
    id: descriptor.id,
1160
    format: descriptor.format,
1161
    width: descriptor.width,
1162
    height: descriptor.height,
1163
    usage: descriptor.usage,
1164
    dimension,
1165
    depth,
1166
    mipLevels,
1167
    samples
1168
  };
1169
}
1170

1171
function normalizeGraphTextureView<Format extends TextureFormat>(
1172
  texture: GraphTextureHandle<Format>,
1173
  props: GraphTextureViewProps
1174
): Required<GraphTextureViewProps> & {width: number; height: number; depth: number} {
1175
  const dimension = props.dimension ?? texture.dimension;
22✔
1176
  const aspect = props.aspect ?? 'all';
22✔
1177
  const baseMipLevel = props.baseMipLevel ?? 0;
22✔
1178
  const mipLevelCount = props.mipLevelCount ?? texture.mipLevels - baseMipLevel;
22✔
1179
  const baseArrayLayer = props.baseArrayLayer ?? 0;
22✔
1180
  const maximumArrayLayerCount = texture.dimension === '3d' ? 1 : texture.depth;
22!
1181
  const arrayLayerCount = props.arrayLayerCount ?? maximumArrayLayerCount - baseArrayLayer;
22✔
1182
  for (const [name, value] of Object.entries({
22✔
1183
    baseMipLevel,
1184
    mipLevelCount,
1185
    baseArrayLayer,
1186
    arrayLayerCount
1187
  })) {
1188
    if (!Number.isSafeInteger(value) || value < 0) {
88!
1189
      throw new Error(`Graph texture view ${name} must be a non-negative safe integer`);
×
1190
    }
1191
  }
1192
  if (mipLevelCount === 0 || baseMipLevel + mipLevelCount > texture.mipLevels) {
22✔
1193
    throw new Error(`Graph texture view exceeds texture "${texture.id}" mip levels`);
2✔
1194
  }
1195
  if (
20!
1196
    arrayLayerCount === 0 ||
60!
1197
    baseArrayLayer + arrayLayerCount > maximumArrayLayerCount ||
1198
    (texture.dimension === '3d' && (baseArrayLayer !== 0 || arrayLayerCount !== 1))
1199
  ) {
1200
    throw new Error(`Graph texture view exceeds texture "${texture.id}" array layers`);
×
1201
  }
1202
  const width = Math.max(1, texture.width >> baseMipLevel);
20✔
1203
  const height = texture.dimension === '1d' ? 1 : Math.max(1, texture.height >> baseMipLevel);
20!
1204
  const depth =
1205
    texture.dimension === '3d' ? Math.max(1, texture.depth >> baseMipLevel) : arrayLayerCount;
20!
1206
  return {
20✔
1207
    dimension,
1208
    aspect,
1209
    baseMipLevel,
1210
    mipLevelCount,
1211
    baseArrayLayer,
1212
    arrayLayerCount,
1213
    width,
1214
    height,
1215
    depth
1216
  };
1217
}
1218

1219
function validateGraphDataView(
1220
  buffer: GraphBufferHandle,
1221
  props: {length: number; byteOffset: number; byteStride: number; rowByteLength: number}
1222
): void {
1223
  for (const [name, value] of Object.entries(props)) {
908✔
1224
    if (!Number.isSafeInteger(value) || value < 0) {
3,632!
1225
      throw new Error(`Graph data view ${name} must be a non-negative safe integer`);
×
1226
    }
1227
  }
1228
  if (props.length > 1 && props.byteStride === 0) {
908!
1229
    throw new Error('Graph data view byteStride must be positive for multiple rows');
×
1230
  }
1231
  if (props.rowByteLength > props.byteStride && props.length > 1) {
908!
1232
    throw new Error('Graph data view rowByteLength cannot exceed byteStride');
×
1233
  }
1234
  const byteLength =
1235
    props.length === 0 ? 0 : (props.length - 1) * props.byteStride + props.rowByteLength;
908✔
1236
  if (props.byteOffset + byteLength > buffer.byteLength) {
908✔
1237
    throw new Error(`Graph data view exceeds buffer "${buffer.id}" byte length`);
2✔
1238
  }
1239
}
1240

1241
function validateImportedBuffer(
1242
  importedBuffer: GraphImportedBuffer,
1243
  descriptor: Pick<GraphBufferDescriptor, 'id' | 'byteLength' | 'usage'>,
1244
  device: Device
1245
): void {
1246
  const buffer = getCoreBuffer(importedBuffer);
556✔
1247
  if (buffer.device !== device) {
556✔
1248
    throw new Error(`GPUCommandGraph buffer "${descriptor.id}" belongs to another device`);
2✔
1249
  }
1250
  if (buffer.byteLength < descriptor.byteLength) {
554✔
1251
    throw new Error(`GPUCommandGraph buffer "${descriptor.id}" is smaller than compiled capacity`);
2✔
1252
  }
1253
  if ((buffer.usage & descriptor.usage) !== descriptor.usage) {
552!
1254
    throw new Error(`GPUCommandGraph buffer "${descriptor.id}" has incompatible usage flags`);
×
1255
  }
1256
}
1257

1258
function validateImportedTexture(
1259
  importedTexture: GraphImportedTexture,
1260
  descriptor: NormalizedGraphTextureDescriptor,
1261
  device: Device
1262
): void {
1263
  const texture = getCoreTexture(importedTexture);
16✔
1264
  if (texture.device !== device) {
16✔
1265
    throw new Error(`GPUCommandGraph texture "${descriptor.id}" belongs to another device`);
2✔
1266
  }
1267
  for (const [name, expected, actual] of [
14✔
1268
    ['format', descriptor.format, texture.format],
1269
    ['dimension', descriptor.dimension, texture.dimension],
1270
    ['width', descriptor.width, texture.width],
1271
    ['height', descriptor.height, texture.height],
1272
    ['depth', descriptor.depth, texture.depth],
1273
    ['mipLevels', descriptor.mipLevels, texture.mipLevels],
1274
    ['samples', descriptor.samples, texture.samples]
1275
  ] as const) {
1276
    if (actual !== expected) {
90✔
1277
      throw new Error(
2✔
1278
        `GPUCommandGraph texture "${descriptor.id}" has incompatible ${name} (${actual} !== ${expected})`
1279
      );
1280
    }
1281
  }
1282
  if ((texture.props.usage & descriptor.usage) !== descriptor.usage) {
12!
1283
    throw new Error(`GPUCommandGraph texture "${descriptor.id}" has incompatible usage flags`);
×
1284
  }
1285
}
1286

1287
function validateBufferUseAgainstDescriptor(
1288
  buffer: GraphBufferHandle,
1289
  usage: GraphBufferUsage
1290
): void {
1291
  const requiredUsage = getRequiredBufferUsage(usage);
2,468✔
1292
  if ((buffer.usage & requiredUsage) !== requiredUsage) {
2,468✔
1293
    throw new Error(
2✔
1294
      `GPUCommandGraph buffer "${buffer.id}" does not declare usage required by ${usage}`
1295
    );
1296
  }
1297
}
1298

1299
function validateTextureUseAgainstDescriptor(
1300
  texture: GraphTextureHandle,
1301
  usage: GraphTextureUsage
1302
): void {
1303
  const requiredUsage = getRequiredTextureUsage(usage);
34✔
1304
  if ((texture.usage & requiredUsage) !== requiredUsage) {
34✔
1305
    throw new Error(
2✔
1306
      `GPUCommandGraph texture "${texture.id}" does not declare usage required by ${usage}`
1307
    );
1308
  }
1309
}
1310

1311
function validateTextureViewForUsage(
1312
  textureOrView: GraphTextureHandle | GraphTextureView,
1313
  usage: GraphTextureUsage
1314
): void {
1315
  if (
32!
1316
    textureOrView instanceof GraphTextureView &&
66✔
1317
    usage.startsWith('storage-') &&
1318
    textureOrView.mipLevelCount !== 1
1319
  ) {
1320
    throw new Error('GPUCommandGraph storage texture views must contain exactly one mip level');
×
1321
  }
1322
}
1323

1324
function getRequiredBufferUsage(usage: GraphBufferUsage): number {
1325
  switch (usage) {
2,468!
1326
    case 'storage-read':
1327
    case 'storage-write':
1328
    case 'storage-read-write':
1329
      return Buffer.STORAGE;
2,466✔
1330
    case 'uniform':
1331
      return Buffer.UNIFORM;
×
1332
    case 'copy-source':
1333
      return Buffer.COPY_SRC;
×
1334
    case 'copy-destination':
1335
      return Buffer.COPY_DST;
2✔
1336
    case 'indirect':
1337
      return Buffer.INDIRECT;
×
1338
    case 'vertex':
1339
      return Buffer.VERTEX;
×
1340
    case 'index':
1341
      return Buffer.INDEX;
×
1342
  }
1343
}
1344

1345
function getRequiredTextureUsage(usage: GraphTextureUsage): number {
1346
  switch (usage) {
34!
1347
    case 'sampled':
1348
      return Texture.SAMPLE;
8✔
1349
    case 'storage-read':
1350
    case 'storage-write':
1351
    case 'storage-read-write':
1352
      return Texture.STORAGE;
14✔
1353
    case 'render-attachment':
1354
      return Texture.RENDER;
10✔
1355
    case 'copy-source':
1356
      return Texture.COPY_SRC;
2✔
1357
    case 'copy-destination':
1358
      return Texture.COPY_DST;
×
1359
  }
1360
}
1361

1362
function isBufferReadUsage(usage: GraphBufferUsage): boolean {
1363
  return (
2,466✔
1364
    usage === 'storage-read' ||
8,648✔
1365
    usage === 'storage-read-write' ||
1366
    usage === 'uniform' ||
1367
    usage === 'copy-source' ||
1368
    usage === 'indirect' ||
1369
    usage === 'vertex' ||
1370
    usage === 'index'
1371
  );
1372
}
1373

1374
function isBufferWriteUsage(usage: GraphBufferUsage): boolean {
1375
  return (
2,466✔
1376
    usage === 'storage-write' || usage === 'storage-read-write' || usage === 'copy-destination'
5,236✔
1377
  );
1378
}
1379

1380
function isTextureReadUsage(usage: GraphTextureUsage): boolean {
1381
  return (
12✔
1382
    usage === 'sampled' ||
30✔
1383
    usage === 'storage-read' ||
1384
    usage === 'storage-read-write' ||
1385
    usage === 'render-attachment' ||
1386
    usage === 'copy-source'
1387
  );
1388
}
1389

1390
function isTextureWriteUsage(usage: GraphTextureUsage): boolean {
1391
  return (
10✔
1392
    usage === 'storage-write' ||
14!
1393
    usage === 'storage-read-write' ||
1394
    usage === 'render-attachment' ||
1395
    usage === 'copy-destination'
1396
  );
1397
}
1398

1399
function getNodeOrder<Parameters>(
1400
  nodes: GPUCommandGraphNode<Parameters>[]
1401
): GPUCommandGraphNode<Parameters>[] {
1402
  const nodeById = new Map(nodes.map(node => [node.id, node]));
912✔
1403
  const dependencies = new Map<string, Set<string>>();
124✔
1404
  const lastBufferWriter = new Map<GraphBufferHandle, string>();
124✔
1405
  const activeBufferReaders = new Map<GraphBufferHandle, Set<string>>();
124✔
1406
  const textureHistory = new Map<
124✔
1407
    GraphTextureHandle,
1408
    {nodeId: string; resource: GraphTextureUse}[]
1409
  >();
1410

1411
  for (const node of nodes) {
124✔
1412
    const nodeDependencies = new Set(node.dependsOn ?? []);
912✔
1413
    for (const dependency of nodeDependencies) {
912✔
1414
      if (!nodeById.has(dependency)) {
14!
1415
        throw new Error(
×
1416
          `GPUCommandGraph node "${node.id}" depends on missing node "${dependency}"`
1417
        );
1418
      }
1419
    }
1420
    for (const resource of node.resources ?? []) {
912✔
1421
      if (isGraphBufferUse(resource)) {
2,498✔
1422
        const handle = getBufferHandle(resource.buffer);
2,466✔
1423
        if (isBufferReadUsage(resource.usage)) {
2,466✔
1424
          const writer = lastBufferWriter.get(handle);
1,462✔
1425
          if (writer) {
1,462✔
1426
            nodeDependencies.add(writer);
1,290✔
1427
          }
1428
          const readers = activeBufferReaders.get(handle) ?? new Set<string>();
1,462✔
1429
          readers.add(node.id);
1,462✔
1430
          activeBufferReaders.set(handle, readers);
1,462✔
1431
        }
1432
        if (isBufferWriteUsage(resource.usage)) {
2,466✔
1433
          const writer = lastBufferWriter.get(handle);
1,162✔
1434
          if (writer) {
1,162✔
1435
            nodeDependencies.add(writer);
434✔
1436
          }
1437
          for (const reader of activeBufferReaders.get(handle) ?? []) {
1,162✔
1438
            if (reader !== node.id) {
556✔
1439
              nodeDependencies.add(reader);
398✔
1440
            }
1441
          }
1442
          activeBufferReaders.set(handle, new Set());
1,162✔
1443
          lastBufferWriter.set(handle, node.id);
1,162✔
1444
        }
1445
      } else {
1446
        const handle = getTextureHandle(resource.texture);
32✔
1447
        const history = textureHistory.get(handle) ?? [];
32✔
1448
        for (const previous of history) {
32✔
1449
          if (
12✔
1450
            previous.nodeId !== node.id &&
46!
1451
            textureUsesOverlap(previous.resource, resource) &&
1452
            ((isTextureReadUsage(resource.usage) && isTextureWriteUsage(previous.resource.usage)) ||
1453
              (isTextureWriteUsage(resource.usage) &&
1454
                (isTextureReadUsage(previous.resource.usage) ||
1455
                  isTextureWriteUsage(previous.resource.usage))))
1456
          ) {
1457
            nodeDependencies.add(previous.nodeId);
10✔
1458
          }
1459
        }
1460
        history.push({nodeId: node.id, resource});
32✔
1461
        textureHistory.set(handle, history);
32✔
1462
      }
1463
    }
1464
    nodeDependencies.delete(node.id);
912✔
1465
    dependencies.set(node.id, nodeDependencies);
912✔
1466
  }
1467

1468
  const insertionIndex = new Map(nodes.map((node, index) => [node.id, index]));
912✔
1469
  const remaining = new Map(
124✔
1470
    Array.from(dependencies, ([id, values]) => [id, new Set(values)] as const)
912✔
1471
  );
1472
  const ordered: GPUCommandGraphNode<Parameters>[] = [];
124✔
1473
  while (remaining.size > 0) {
124✔
1474
    const ready = Array.from(remaining)
900✔
1475
      .filter(([, values]) => values.size === 0)
52,182✔
1476
      .map(([id]) => id)
904✔
1477
      .sort((left, right) => insertionIndex.get(left)! - insertionIndex.get(right)!);
8✔
1478
    if (ready.length === 0) {
900✔
1479
      throw new Error('GPUCommandGraph contains a dependency cycle');
4✔
1480
    }
1481
    for (const id of ready) {
896✔
1482
      ordered.push(nodeById.get(id)!);
904✔
1483
      remaining.delete(id);
904✔
1484
      for (const values of remaining.values()) {
904✔
1485
        values.delete(id);
51,286✔
1486
      }
1487
    }
1488
  }
1489
  return ordered;
120✔
1490
}
1491

1492
function textureUsesOverlap(left: GraphTextureUse, right: GraphTextureUse): boolean {
1493
  const leftHandle = getTextureHandle(left.texture);
12✔
1494
  const rightHandle = getTextureHandle(right.texture);
12✔
1495
  if (leftHandle !== rightHandle) {
12!
1496
    return false;
×
1497
  }
1498
  const leftRange = getTextureSubresourceRange(left.texture);
12✔
1499
  const rightRange = getTextureSubresourceRange(right.texture);
12✔
1500
  return (
12✔
1501
    aspectsOverlap(leftRange.aspect, rightRange.aspect) &&
34✔
1502
    intervalsOverlap(
1503
      leftRange.baseMipLevel,
1504
      leftRange.mipLevelCount,
1505
      rightRange.baseMipLevel,
1506
      rightRange.mipLevelCount
1507
    ) &&
1508
    intervalsOverlap(
1509
      leftRange.baseArrayLayer,
1510
      leftRange.arrayLayerCount,
1511
      rightRange.baseArrayLayer,
1512
      rightRange.arrayLayerCount
1513
    )
1514
  );
1515
}
1516

1517
function getTextureSubresourceRange(texture: GraphTextureHandle | GraphTextureView): {
1518
  aspect: GraphTextureAspect;
1519
  baseMipLevel: number;
1520
  mipLevelCount: number;
1521
  baseArrayLayer: number;
1522
  arrayLayerCount: number;
1523
} {
1524
  if (texture instanceof GraphTextureView) {
24✔
1525
    return texture;
16✔
1526
  }
1527
  return {
8✔
1528
    aspect: 'all',
1529
    baseMipLevel: 0,
1530
    mipLevelCount: texture.mipLevels,
1531
    baseArrayLayer: 0,
1532
    arrayLayerCount: texture.dimension === '3d' ? 1 : texture.depth
8!
1533
  };
1534
}
1535

1536
function aspectsOverlap(left: GraphTextureAspect, right: GraphTextureAspect): boolean {
1537
  return left === 'all' || right === 'all' || left === right;
12!
1538
}
1539

1540
function intervalsOverlap(
1541
  leftStart: number,
1542
  leftCount: number,
1543
  rightStart: number,
1544
  rightCount: number
1545
): boolean {
1546
  return leftStart < rightStart + rightCount && rightStart < leftStart + leftCount;
22✔
1547
}
1548

1549
function getBufferTransientAllocationPlan<Parameters>(
1550
  nodes: GPUCommandGraphNode<Parameters>[],
1551
  buffers: Iterable<GraphBufferHandle>
1552
): BufferTransientAllocation[] {
1553
  const lifetimes = getResourceLifetimes(nodes, resource =>
120✔
1554
    isGraphBufferUse(resource) ? getBufferHandle(resource.buffer) : null
2,494✔
1555
  );
1556
  const allocations: BufferTransientAllocation[] = [];
120✔
1557
  const transientBuffers = Array.from(buffers)
120✔
1558
    .filter(buffer => buffer.transient)
870✔
1559
    .map(buffer => ({buffer, lifetime: lifetimes.get(buffer)}))
606✔
1560
    .sort(
1561
      (left, right) =>
886✔
1562
        (left.lifetime?.firstUse ?? Number.MAX_SAFE_INTEGER) -
886!
1563
        (right.lifetime?.firstUse ?? Number.MAX_SAFE_INTEGER)
886!
1564
    );
1565

1566
  for (const {buffer, lifetime} of transientBuffers) {
120✔
1567
    if (!lifetime) {
606!
1568
      continue;
×
1569
    }
1570
    let allocation = allocations
606✔
1571
      .filter(candidate => candidate.lastUse < lifetime.firstUse)
3,034✔
1572
      .sort((left, right) => left.byteLength - right.byteLength)[0];
856✔
1573
    if (!allocation) {
606✔
1574
      allocation = {byteLength: 0, usage: 0, lastUse: -1, handles: []};
108✔
1575
      allocations.push(allocation);
108✔
1576
    }
1577
    allocation.byteLength = Math.max(allocation.byteLength, buffer.byteLength);
606✔
1578
    allocation.usage |= buffer.usage;
606✔
1579
    allocation.lastUse = lifetime.lastUse;
606✔
1580
    allocation.handles.push(buffer);
606✔
1581
  }
1582
  return allocations;
120✔
1583
}
1584

1585
function getTextureTransientAllocationPlan<Parameters>(
1586
  nodes: GPUCommandGraphNode<Parameters>[],
1587
  textures: Iterable<GraphTextureHandle>
1588
): TextureTransientAllocation[] {
1589
  const lifetimes = getResourceLifetimes(nodes, resource =>
120✔
1590
    isGraphBufferUse(resource) ? null : getTextureHandle(resource.texture)
2,494✔
1591
  );
1592
  const allocations: TextureTransientAllocation[] = [];
120✔
1593
  const transientTextures = Array.from(textures)
120✔
1594
    .filter(texture => texture.transient)
18✔
1595
    .map(texture => ({texture, lifetime: lifetimes.get(texture)}))
14✔
1596
    .sort(
1597
      (left, right) =>
6✔
1598
        (left.lifetime?.firstUse ?? Number.MAX_SAFE_INTEGER) -
6!
1599
        (right.lifetime?.firstUse ?? Number.MAX_SAFE_INTEGER)
6!
1600
    );
1601

1602
  for (const {texture, lifetime} of transientTextures) {
120✔
1603
    if (!lifetime) {
14!
1604
      continue;
×
1605
    }
1606
    let allocation = allocations.find(
14✔
1607
      candidate =>
1608
        candidate.lastUse < lifetime.firstUse &&
8✔
1609
        areTextureDescriptorsCompatible(candidate.descriptor, texture)
1610
    );
1611
    if (!allocation) {
14✔
1612
      const descriptor = getNormalizedTextureDescriptor(texture);
12✔
1613
      allocation = {
12✔
1614
        descriptor,
1615
        byteLength: getTextureByteLength(texture),
1616
        lastUse: -1,
1617
        handles: []
1618
      };
1619
      allocations.push(allocation);
12✔
1620
    }
1621
    allocation.descriptor.usage |= texture.usage;
14✔
1622
    allocation.lastUse = lifetime.lastUse;
14✔
1623
    allocation.handles.push(texture);
14✔
1624
  }
1625
  return allocations;
120✔
1626
}
1627

1628
function getResourceLifetimes<Parameters, Resource extends object>(
1629
  nodes: GPUCommandGraphNode<Parameters>[],
1630
  getResource: (resource: GraphResourceUse) => Resource | null
1631
): Map<Resource, {firstUse: number; lastUse: number}> {
1632
  const lifetimes = new Map<Resource, {firstUse: number; lastUse: number}>();
240✔
1633
  nodes.forEach((node, nodeIndex) => {
240✔
1634
    for (const resourceUse of node.resources ?? []) {
1,808✔
1635
      const resource = getResource(resourceUse);
4,988✔
1636
      if (!resource || !('transient' in resource) || !resource.transient) {
4,988✔
1637
        continue;
3,124✔
1638
      }
1639
      const lifetime = lifetimes.get(resource);
1,864✔
1640
      if (lifetime) {
1,864✔
1641
        lifetime.lastUse = nodeIndex;
1,244✔
1642
      } else {
1643
        lifetimes.set(resource, {firstUse: nodeIndex, lastUse: nodeIndex});
620✔
1644
      }
1645
    }
1646
  });
1647
  return lifetimes;
240✔
1648
}
1649

1650
function getNormalizedTextureDescriptor(
1651
  texture: GraphTextureHandle
1652
): NormalizedGraphTextureDescriptor {
1653
  return {
12✔
1654
    id: texture.id,
1655
    format: texture.format,
1656
    width: texture.width,
1657
    height: texture.height,
1658
    usage: texture.usage,
1659
    dimension: texture.dimension,
1660
    depth: texture.depth,
1661
    mipLevels: texture.mipLevels,
1662
    samples: texture.samples
1663
  };
1664
}
1665

1666
function areTextureDescriptorsCompatible(
1667
  descriptor: NormalizedGraphTextureDescriptor,
1668
  texture: GraphTextureHandle
1669
): boolean {
1670
  return (
2✔
1671
    descriptor.format === texture.format &&
14✔
1672
    descriptor.width === texture.width &&
1673
    descriptor.height === texture.height &&
1674
    descriptor.dimension === texture.dimension &&
1675
    descriptor.depth === texture.depth &&
1676
    descriptor.mipLevels === texture.mipLevels &&
1677
    descriptor.samples === texture.samples
1678
  );
1679
}
1680

1681
function getTextureByteLength(texture: GraphTextureHandle): number {
1682
  let byteLength = 0;
26✔
1683
  for (let mipLevel = 0; mipLevel < texture.mipLevels; mipLevel++) {
26✔
1684
    byteLength += textureFormatDecoder.computeMemoryLayout({
30✔
1685
      format: texture.format,
1686
      width: Math.max(1, texture.width >> mipLevel),
1687
      height: texture.dimension === '1d' ? 1 : Math.max(1, texture.height >> mipLevel),
30!
1688
      depth: texture.dimension === '3d' ? Math.max(1, texture.depth >> mipLevel) : texture.depth,
30!
1689
      byteAlignment: 1
1690
    }).byteLength;
1691
  }
1692
  return byteLength * texture.samples;
26✔
1693
}
1694

1695
function isDefaultGraphTextureView(view: GraphTextureView): boolean {
1696
  const texture = view.texture;
36✔
1697
  return (
36✔
1698
    view.dimension === texture.dimension &&
216✔
1699
    view.aspect === 'all' &&
1700
    view.baseMipLevel === 0 &&
1701
    view.mipLevelCount === texture.mipLevels &&
1702
    view.baseArrayLayer === 0 &&
1703
    view.arrayLayerCount === (texture.dimension === '3d' ? 1 : texture.depth)
36!
1704
  );
1705
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc