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

visgl / luma.gl / 28677891986

03 Jul 2026 06:37PM UTC coverage: 72.426%. First build
28677891986

push

github

web-flow
chore(experimental) Extract and document GPU command graph compiler (#2722)

10952 of 17009 branches covered (64.39%)

Branch coverage included in aggregate %.

278 of 288 new or added lines in 8 files covered. (96.53%)

21345 of 27584 relevant lines covered (77.38%)

5729.82 hits per line

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

92.44
/modules/experimental/src/gpu-primitives/gpu-command-graph-compiler.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 {Device, TextureFormat} from '@luma.gl/core';
7
import type {
8
  GPUCommandGraphComputeExecutable,
9
  GPUCommandGraphCopyExecutable,
10
  GPUCommandGraphNode,
11
  GPUCommandGraphRenderExecutable,
12
  GPUCommandGraphStats,
13
  GraphBufferHandle,
14
  GraphBufferUsage,
15
  GraphBufferUse,
16
  GraphDataView,
17
  GraphResourceUse,
18
  GraphTextureAspect,
19
  GraphTextureDimension,
20
  GraphTextureHandle,
21
  GraphTextureUse,
22
  GraphTextureUsage,
23
  GraphTextureView
24
} from './gpu-command-graph-types';
25

26
/** A graph node paired with the reusable executable produced by its compile callback. @internal */
27
export type CompiledNode<Parameters> = {
28
  /** Scheduled node definition. */
29
  node: GPUCommandGraphNode<Parameters>;
30
  /** Reusable node state and encode callback. */
31
  executable:
32
    | GPUCommandGraphComputeExecutable<Parameters>
33
    | GPUCommandGraphRenderExecutable<Parameters>
34
    | GPUCommandGraphCopyExecutable<Parameters>;
35
};
36

37
/** One physical buffer allocation shared by logical transients with disjoint lifetimes. @internal */
38
export type BufferTransientAllocation = {
39
  /** Maximum capacity required by any assigned handle. */
40
  byteLength: number;
41
  /** Union of usage flags required by assigned handles. */
42
  usage: number;
43
  /** Last scheduled node index using the most recently assigned handle. */
44
  lastUse: number;
45
  /** Logical handles assigned to this allocation in lifetime order. */
46
  handles: GraphBufferHandle[];
47
  /** Physical buffer created during compilation. */
48
  buffer?: Buffer;
49
};
50

51
type TransientTextureDescriptor = {
52
  id: string;
53
  format: TextureFormat;
54
  width: number;
55
  height: number;
56
  usage: number;
57
  dimension: GraphTextureDimension;
58
  depth: number;
59
  mipLevels: number;
60
  samples: number;
61
};
62

63
/** One physical texture allocation shared by compatible transients with disjoint lifetimes. @internal */
64
export type TextureTransientAllocation = {
65
  /** Descriptor shared by every assigned handle, with their usage flags combined. */
66
  descriptor: TransientTextureDescriptor;
67
  /** Estimated physical allocation size used for statistics. */
68
  byteLength: number;
69
  /** Last scheduled node index using the most recently assigned handle. */
70
  lastUse: number;
71
  /** Logical handles assigned to this allocation in lifetime order. */
72
  handles: GraphTextureHandle[];
73
  /** Physical texture created during compilation. */
74
  texture?: Texture;
75
};
76

77
/** Complete immutable input for constructing a `CompiledGPUCommandGraph`. @internal */
78
export type GPUCommandGraphCompilation<Parameters> = {
79
  /** Device owning compiled resources. */
80
  device: Device;
81
  /** Graph identifier. */
82
  id: string;
83
  /** Snapshot of logical buffer resources. */
84
  buffers: Map<string, GraphBufferHandle>;
85
  /** Snapshot of logical texture resources. */
86
  textures: Map<string, GraphTextureHandle>;
87
  /** Nodes and executables in stable topological order. */
88
  compiledNodes: CompiledNode<Parameters>[];
89
  /** Logical-to-physical transient buffer mapping. */
90
  transientBuffers: Map<GraphBufferHandle, Buffer>;
91
  /** Logical-to-physical transient texture mapping. */
92
  transientTextures: Map<GraphTextureHandle, Texture>;
93
  /** Physical buffer allocation plan retained for destruction. */
94
  bufferTransientAllocations: BufferTransientAllocation[];
95
  /** Physical texture allocation plan retained for destruction. */
96
  textureTransientAllocations: TextureTransientAllocation[];
97
  /** Scheduling and allocation statistics. */
98
  stats: GPUCommandGraphStats;
99
};
100

101
/**
102
 * Compiles scheduling, transient allocations, and executable node resources.
103
 *
104
 * Compilation proceeds in four phases: infer a stable topological node order, plan allocation
105
 * reuse from transient lifetimes, create physical resources, then invoke node compile callbacks.
106
 * If a callback fails, every executable and physical resource created so far is destroyed before
107
 * the error is rethrown.
108
 *
109
 * @internal
110
 */
111
export function compileGPUCommandGraph<Parameters>(props: {
112
  device: Device;
113
  id: string;
114
  buffers: Map<string, GraphBufferHandle>;
115
  textures: Map<string, GraphTextureHandle>;
116
  nodes: GPUCommandGraphNode<Parameters>[];
117
}): GPUCommandGraphCompilation<Parameters> {
118
  const nodeOrder = getNodeOrder(props.nodes);
73✔
119
  const bufferPlan = getBufferTransientAllocationPlan(nodeOrder, props.buffers.values());
71✔
120
  const texturePlan = getTextureTransientAllocationPlan(nodeOrder, props.textures.values());
71✔
121
  const transientBuffers = new Map<GraphBufferHandle, Buffer>();
71✔
122
  const transientTextures = new Map<GraphTextureHandle, Texture>();
71✔
123

124
  for (const allocation of bufferPlan) {
71✔
125
    allocation.buffer = props.device.createBuffer({
77✔
126
      id: `${props.id}-transient-buffer-${bufferPlan.indexOf(allocation)}`,
127
      byteLength: allocation.byteLength,
128
      usage: allocation.usage
129
    });
130
    for (const handle of allocation.handles) {
77✔
131
      transientBuffers.set(handle, allocation.buffer);
328✔
132
    }
133
  }
134
  for (const allocation of texturePlan) {
71✔
135
    allocation.texture = props.device.createTexture({
6✔
136
      ...allocation.descriptor,
137
      id: `${props.id}-transient-texture-${texturePlan.indexOf(allocation)}`
138
    });
139
    for (const handle of allocation.handles) {
6✔
140
      transientTextures.set(handle, allocation.texture);
7✔
141
    }
142
  }
143

144
  const compiledNodes: CompiledNode<Parameters>[] = [];
71✔
145
  try {
71✔
146
    for (const node of nodeOrder) {
71✔
147
      compiledNodes.push({node, executable: node.compile({device: props.device})});
503✔
148
    }
149
  } catch (error) {
NEW
150
    for (const compiledNode of compiledNodes) {
×
NEW
151
      compiledNode.executable.destroy?.();
×
152
    }
NEW
153
    for (const allocation of bufferPlan) {
×
NEW
154
      allocation.buffer?.destroy();
×
155
    }
NEW
156
    for (const allocation of texturePlan) {
×
NEW
157
      allocation.texture?.destroy();
×
158
    }
NEW
159
    throw error;
×
160
  }
161

162
  const logicalTransientBytes = Array.from(props.buffers.values())
71✔
163
    .filter(buffer => buffer.transient)
511✔
164
    .reduce((sum, buffer) => sum + buffer.byteLength, 0);
329✔
165
  const physicalTransientBytes = bufferPlan.reduce(
71✔
166
    (sum, allocation) => sum + allocation.byteLength,
77✔
167
    0
168
  );
169
  const reusedTransientBytes = Math.max(0, logicalTransientBytes - physicalTransientBytes);
71✔
170
  const logicalTransientTextureBytes = Array.from(props.textures.values())
71✔
171
    .filter(texture => texture.transient)
9✔
172
    .reduce((sum, texture) => sum + getTextureByteLength(texture), 0);
7✔
173
  const physicalTransientTextureBytes = texturePlan.reduce(
71✔
174
    (sum, allocation) => sum + allocation.byteLength,
6✔
175
    0
176
  );
177
  const reusedTransientTextureBytes = Math.max(
71✔
178
    0,
179
    logicalTransientTextureBytes - physicalTransientTextureBytes
180
  );
181
  const stats: GPUCommandGraphStats = {
71✔
182
    nodeOrder: nodeOrder.map(node => node.id),
503✔
183
    logicalTransientBufferCount: Array.from(props.buffers.values()).filter(
184
      buffer => buffer.transient
511✔
185
    ).length,
186
    physicalTransientBufferCount: bufferPlan.length,
187
    logicalTransientBytes,
188
    physicalTransientBytes,
189
    reusedTransientBytes,
190
    reusePercentage:
191
      logicalTransientBytes > 0 ? (reusedTransientBytes / logicalTransientBytes) * 100 : 0,
71✔
192
    logicalTransientTextureCount: Array.from(props.textures.values()).filter(
193
      texture => texture.transient
9✔
194
    ).length,
195
    physicalTransientTextureCount: texturePlan.length,
196
    logicalTransientTextureBytes,
197
    physicalTransientTextureBytes,
198
    reusedTransientTextureBytes,
199
    textureReusePercentage:
200
      logicalTransientTextureBytes > 0
71✔
201
        ? (reusedTransientTextureBytes / logicalTransientTextureBytes) * 100
202
        : 0
203
  };
204

205
  return {
71✔
206
    device: props.device,
207
    id: props.id,
208
    buffers: new Map(props.buffers),
209
    textures: new Map(props.textures),
210
    compiledNodes,
211
    transientBuffers,
212
    transientTextures,
213
    bufferTransientAllocations: bufferPlan,
214
    textureTransientAllocations: texturePlan,
215
    stats
216
  };
217
}
218

219
/** Returns the underlying logical buffer for either a handle or typed data view. @internal */
220
export function getBufferHandle(buffer: GraphBufferHandle | GraphDataView): GraphBufferHandle {
221
  return 'buffer' in buffer ? buffer.buffer : buffer;
5,465✔
222
}
223

224
/** Returns the underlying logical texture for either a handle or subresource view. @internal */
225
export function getTextureHandle(
226
  texture: GraphTextureHandle | GraphTextureView
227
): GraphTextureHandle {
228
  return 'texture' in texture ? texture.texture : texture;
80✔
229
}
230

231
/** Narrows a declared resource use to a buffer use. @internal */
232
export function isGraphBufferUse(resource: GraphResourceUse): resource is GraphBufferUse {
233
  return 'buffer' in resource;
5,514✔
234
}
235

236
/** Returns whether an access observes existing buffer contents. */
237
function isBufferReadUsage(usage: GraphBufferUsage): boolean {
238
  return (
1,363✔
239
    usage === 'storage-read' ||
4,786✔
240
    usage === 'storage-read-write' ||
241
    usage === 'uniform' ||
242
    usage === 'copy-source' ||
243
    usage === 'indirect' ||
244
    usage === 'vertex' ||
245
    usage === 'index'
246
  );
247
}
248

249
/** Returns whether an access changes buffer contents. */
250
function isBufferWriteUsage(usage: GraphBufferUsage): boolean {
251
  return (
1,363✔
252
    usage === 'storage-write' || usage === 'storage-read-write' || usage === 'copy-destination'
2,888✔
253
  );
254
}
255

256
/** Returns whether an access observes existing texture contents. */
257
function isTextureReadUsage(usage: GraphTextureUsage): boolean {
258
  return (
6✔
259
    usage === 'sampled' ||
15✔
260
    usage === 'storage-read' ||
261
    usage === 'storage-read-write' ||
262
    usage === 'render-attachment' ||
263
    usage === 'copy-source'
264
  );
265
}
266

267
/** Returns whether an access may change texture contents. */
268
function isTextureWriteUsage(usage: GraphTextureUsage): boolean {
269
  return (
5✔
270
    usage === 'storage-write' ||
7!
271
    usage === 'storage-read-write' ||
272
    usage === 'render-attachment' ||
273
    usage === 'copy-destination'
274
  );
275
}
276

277
/**
278
 * Infers hazards and returns a stable topological node order.
279
 *
280
 * Buffer hazards are conservative at whole-handle granularity. Texture hazards use overlapping
281
 * aspect, mip, and layer ranges. Explicit dependencies and inferred hazards share the same graph;
282
 * nodes that are otherwise independent retain insertion order.
283
 */
284
function getNodeOrder<Parameters>(
285
  nodes: GPUCommandGraphNode<Parameters>[]
286
): GPUCommandGraphNode<Parameters>[] {
287
  const nodeById = new Map(nodes.map(node => [node.id, node]));
507✔
288
  const dependencies = new Map<string, Set<string>>();
73✔
289
  const lastBufferWriter = new Map<GraphBufferHandle, string>();
73✔
290
  const activeBufferReaders = new Map<GraphBufferHandle, Set<string>>();
73✔
291
  const textureHistory = new Map<
73✔
292
    GraphTextureHandle,
293
    {nodeId: string; resource: GraphTextureUse}[]
294
  >();
295

296
  for (const node of nodes) {
73✔
297
    const nodeDependencies = new Set(node.dependsOn ?? []);
507✔
298
    for (const dependency of nodeDependencies) {
507✔
299
      if (!nodeById.has(dependency)) {
7!
NEW
300
        throw new Error(
×
301
          `GPUCommandGraph node "${node.id}" depends on missing node "${dependency}"`
302
        );
303
      }
304
    }
305
    for (const resource of node.resources ?? []) {
507✔
306
      if (isGraphBufferUse(resource)) {
1,379✔
307
        const handle = getBufferHandle(resource.buffer);
1,363✔
308
        if (isBufferReadUsage(resource.usage)) {
1,363✔
309
          const writer = lastBufferWriter.get(handle);
808✔
310
          if (writer) {
808✔
311
            nodeDependencies.add(writer);
690✔
312
          }
313
          const readers = activeBufferReaders.get(handle) ?? new Set<string>();
808✔
314
          readers.add(node.id);
808✔
315
          activeBufferReaders.set(handle, readers);
808✔
316
        }
317
        if (isBufferWriteUsage(resource.usage)) {
1,363✔
318
          const writer = lastBufferWriter.get(handle);
648✔
319
          if (writer) {
648✔
320
            nodeDependencies.add(writer);
245✔
321
          }
322
          for (const reader of activeBufferReaders.get(handle) ?? []) {
648✔
323
            if (reader !== node.id) {
292✔
324
              nodeDependencies.add(reader);
199✔
325
            }
326
          }
327
          activeBufferReaders.set(handle, new Set());
648✔
328
          lastBufferWriter.set(handle, node.id);
648✔
329
        }
330
      } else {
331
        const handle = getTextureHandle(resource.texture);
16✔
332
        const history = textureHistory.get(handle) ?? [];
16✔
333
        for (const previous of history) {
16✔
334
          if (
6✔
335
            previous.nodeId !== node.id &&
23!
336
            textureUsesOverlap(previous.resource, resource) &&
337
            ((isTextureReadUsage(resource.usage) && isTextureWriteUsage(previous.resource.usage)) ||
338
              (isTextureWriteUsage(resource.usage) &&
339
                (isTextureReadUsage(previous.resource.usage) ||
340
                  isTextureWriteUsage(previous.resource.usage))))
341
          ) {
342
            nodeDependencies.add(previous.nodeId);
5✔
343
          }
344
        }
345
        history.push({nodeId: node.id, resource});
16✔
346
        textureHistory.set(handle, history);
16✔
347
      }
348
    }
349
    nodeDependencies.delete(node.id);
507✔
350
    dependencies.set(node.id, nodeDependencies);
507✔
351
  }
352

353
  const insertionIndex = new Map(nodes.map((node, index) => [node.id, index]));
507✔
354
  const remaining = new Map(
73✔
355
    Array.from(dependencies, ([id, values]) => [id, new Set(values)] as const)
507✔
356
  );
357
  const ordered: GPUCommandGraphNode<Parameters>[] = [];
73✔
358
  while (remaining.size > 0) {
73✔
359
    const ready = Array.from(remaining)
495✔
360
      .filter(([, values]) => values.size === 0)
26,246✔
361
      .map(([id]) => id)
503✔
362
      .sort((left, right) => insertionIndex.get(left)! - insertionIndex.get(right)!);
10✔
363
    if (ready.length === 0) {
495✔
364
      throw new Error('GPUCommandGraph contains a dependency cycle');
2✔
365
    }
366
    for (const id of ready) {
493✔
367
      ordered.push(nodeById.get(id)!);
503✔
368
      remaining.delete(id);
503✔
369
      for (const values of remaining.values()) {
503✔
370
        values.delete(id);
25,769✔
371
      }
372
    }
373
  }
374
  return ordered;
71✔
375
}
376

377
/** Returns whether two uses refer to overlapping subresources of the same logical texture. */
378
function textureUsesOverlap(left: GraphTextureUse, right: GraphTextureUse): boolean {
379
  const leftHandle = getTextureHandle(left.texture);
6✔
380
  const rightHandle = getTextureHandle(right.texture);
6✔
381
  if (leftHandle !== rightHandle) {
6!
NEW
382
    return false;
×
383
  }
384
  const leftRange = getTextureSubresourceRange(left.texture);
6✔
385
  const rightRange = getTextureSubresourceRange(right.texture);
6✔
386
  return (
6✔
387
    aspectsOverlap(leftRange.aspect, rightRange.aspect) &&
17✔
388
    intervalsOverlap(
389
      leftRange.baseMipLevel,
390
      leftRange.mipLevelCount,
391
      rightRange.baseMipLevel,
392
      rightRange.mipLevelCount
393
    ) &&
394
    intervalsOverlap(
395
      leftRange.baseArrayLayer,
396
      leftRange.arrayLayerCount,
397
      rightRange.baseArrayLayer,
398
      rightRange.arrayLayerCount
399
    )
400
  );
401
}
402

403
/** Normalizes a whole texture or view to the range used for hazard overlap tests. */
404
function getTextureSubresourceRange(texture: GraphTextureHandle | GraphTextureView): {
405
  aspect: GraphTextureAspect;
406
  baseMipLevel: number;
407
  mipLevelCount: number;
408
  baseArrayLayer: number;
409
  arrayLayerCount: number;
410
} {
411
  if ('texture' in texture) {
12✔
412
    return texture;
8✔
413
  }
414
  return {
4✔
415
    aspect: 'all',
416
    baseMipLevel: 0,
417
    mipLevelCount: texture.mipLevels,
418
    baseArrayLayer: 0,
419
    arrayLayerCount: texture.dimension === '3d' ? 1 : texture.depth
4!
420
  };
421
}
422

423
/** Returns whether two depth/stencil aspect selections overlap. */
424
function aspectsOverlap(left: GraphTextureAspect, right: GraphTextureAspect): boolean {
425
  return left === 'all' || right === 'all' || left === right;
6!
426
}
427

428
/** Returns whether two half-open integer intervals overlap. */
429
function intervalsOverlap(
430
  leftStart: number,
431
  leftCount: number,
432
  rightStart: number,
433
  rightCount: number
434
): boolean {
435
  return leftStart < rightStart + rightCount && rightStart < leftStart + leftCount;
11✔
436
}
437

438
/**
439
 * Assigns transient buffers to reusable physical allocations.
440
 *
441
 * The earliest available allocation is selected by smallest capacity. Capacity grows to the
442
 * largest assigned handle and usage flags are combined across all assigned handles.
443
 */
444
function getBufferTransientAllocationPlan<Parameters>(
445
  nodes: GPUCommandGraphNode<Parameters>[],
446
  buffers: Iterable<GraphBufferHandle>
447
): BufferTransientAllocation[] {
448
  const lifetimes = getResourceLifetimes(nodes, resource =>
71✔
449
    isGraphBufferUse(resource) ? getBufferHandle(resource.buffer) : null
1,377✔
450
  );
451
  const allocations: BufferTransientAllocation[] = [];
71✔
452
  const transientBuffers = Array.from(buffers)
71✔
453
    .filter(buffer => buffer.transient)
511✔
454
    .map(buffer => ({buffer, lifetime: lifetimes.get(buffer)}))
329✔
455
    .sort(
456
      (left, right) =>
472✔
457
        (left.lifetime?.firstUse ?? Number.MAX_SAFE_INTEGER) -
473✔
458
        (right.lifetime?.firstUse ?? Number.MAX_SAFE_INTEGER)
474✔
459
    );
460

461
  for (const {buffer, lifetime} of transientBuffers) {
71✔
462
    if (!lifetime) {
329✔
463
      continue;
1✔
464
    }
465
    let allocation = allocations
328✔
466
      .filter(candidate => candidate.lastUse < lifetime.firstUse)
1,553✔
467
      .sort((left, right) => left.byteLength - right.byteLength)[0];
430✔
468
    if (!allocation) {
328✔
469
      allocation = {byteLength: 0, usage: 0, lastUse: -1, handles: []};
77✔
470
      allocations.push(allocation);
77✔
471
    }
472
    allocation.byteLength = Math.max(allocation.byteLength, buffer.byteLength);
328✔
473
    allocation.usage |= buffer.usage;
328✔
474
    allocation.lastUse = lifetime.lastUse;
328✔
475
    allocation.handles.push(buffer);
328✔
476
  }
477
  return allocations;
71✔
478
}
479

480
/**
481
 * Assigns transient textures to descriptor-compatible physical allocations with disjoint
482
 * lifetimes. Unlike buffers, texture extent and sampling properties cannot grow during reuse.
483
 */
484
function getTextureTransientAllocationPlan<Parameters>(
485
  nodes: GPUCommandGraphNode<Parameters>[],
486
  textures: Iterable<GraphTextureHandle>
487
): TextureTransientAllocation[] {
488
  const lifetimes = getResourceLifetimes(nodes, resource =>
71✔
489
    isGraphBufferUse(resource) ? null : getTextureHandle(resource.texture)
1,377✔
490
  );
491
  const allocations: TextureTransientAllocation[] = [];
71✔
492
  const transientTextures = Array.from(textures)
71✔
493
    .filter(texture => texture.transient)
9✔
494
    .map(texture => ({texture, lifetime: lifetimes.get(texture)}))
7✔
495
    .sort(
496
      (left, right) =>
3✔
497
        (left.lifetime?.firstUse ?? Number.MAX_SAFE_INTEGER) -
3!
498
        (right.lifetime?.firstUse ?? Number.MAX_SAFE_INTEGER)
3!
499
    );
500

501
  for (const {texture, lifetime} of transientTextures) {
71✔
502
    if (!lifetime) {
7!
NEW
503
      continue;
×
504
    }
505
    let allocation = allocations.find(
7✔
506
      candidate =>
507
        candidate.lastUse < lifetime.firstUse &&
4✔
508
        areTextureDescriptorsCompatible(candidate.descriptor, texture)
509
    );
510
    if (!allocation) {
7✔
511
      const descriptor = getNormalizedTextureDescriptor(texture);
6✔
512
      allocation = {
6✔
513
        descriptor,
514
        byteLength: getTextureByteLength(texture),
515
        lastUse: -1,
516
        handles: []
517
      };
518
      allocations.push(allocation);
6✔
519
    }
520
    allocation.descriptor.usage |= texture.usage;
7✔
521
    allocation.lastUse = lifetime.lastUse;
7✔
522
    allocation.handles.push(texture);
7✔
523
  }
524
  return allocations;
71✔
525
}
526

527
/** Returns inclusive first/last scheduled use indices for each referenced transient resource. */
528
function getResourceLifetimes<Parameters, Resource extends object>(
529
  nodes: GPUCommandGraphNode<Parameters>[],
530
  getResource: (resource: GraphResourceUse) => Resource | null
531
): Map<Resource, {firstUse: number; lastUse: number}> {
532
  const lifetimes = new Map<Resource, {firstUse: number; lastUse: number}>();
142✔
533
  nodes.forEach((node, nodeIndex) => {
142✔
534
    for (const resourceUse of node.resources ?? []) {
1,006✔
535
      const resource = getResource(resourceUse);
2,754✔
536
      if (!resource || !('transient' in resource) || !resource.transient) {
2,754✔
537
        continue;
1,752✔
538
      }
539
      const lifetime = lifetimes.get(resource);
1,002✔
540
      if (lifetime) {
1,002✔
541
        lifetime.lastUse = nodeIndex;
667✔
542
      } else {
543
        lifetimes.set(resource, {firstUse: nodeIndex, lastUse: nodeIndex});
335✔
544
      }
545
    }
546
  });
547
  return lifetimes;
142✔
548
}
549

550
/** Copies a logical texture handle into the mutable physical-allocation descriptor. */
551
function getNormalizedTextureDescriptor(texture: GraphTextureHandle): TransientTextureDescriptor {
552
  return {
6✔
553
    id: texture.id,
554
    format: texture.format,
555
    width: texture.width,
556
    height: texture.height,
557
    usage: texture.usage,
558
    dimension: texture.dimension,
559
    depth: texture.depth,
560
    mipLevels: texture.mipLevels,
561
    samples: texture.samples
562
  };
563
}
564

565
/** Returns whether a logical texture can reuse an existing physical texture allocation. */
566
function areTextureDescriptorsCompatible(
567
  descriptor: TransientTextureDescriptor,
568
  texture: GraphTextureHandle
569
): boolean {
570
  return (
1✔
571
    descriptor.format === texture.format &&
7✔
572
    descriptor.width === texture.width &&
573
    descriptor.height === texture.height &&
574
    descriptor.dimension === texture.dimension &&
575
    descriptor.depth === texture.depth &&
576
    descriptor.mipLevels === texture.mipLevels &&
577
    descriptor.samples === texture.samples
578
  );
579
}
580

581
/** Estimates all mip and sample storage for texture reuse statistics. */
582
function getTextureByteLength(texture: GraphTextureHandle): number {
583
  let byteLength = 0;
13✔
584
  for (let mipLevel = 0; mipLevel < texture.mipLevels; mipLevel++) {
13✔
585
    byteLength += textureFormatDecoder.computeMemoryLayout({
15✔
586
      format: texture.format,
587
      width: Math.max(1, texture.width >> mipLevel),
588
      height: texture.dimension === '1d' ? 1 : Math.max(1, texture.height >> mipLevel),
15!
589
      depth: texture.dimension === '3d' ? Math.max(1, texture.depth >> mipLevel) : texture.depth,
15!
590
      byteAlignment: 1
591
    }).byteLength;
592
  }
593
  return byteLength * texture.samples;
13✔
594
}
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