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

visgl / luma.gl / 28205988640

25 Jun 2026 11:03PM UTC coverage: 71.259% (+0.8%) from 70.501%
28205988640

Pull #2701

github

web-flow
Merge 4980c2ae8 into cb506267f
Pull Request #2701: [codex] Add experimental GPU command graph

10146 of 16126 branches covered (62.92%)

Branch coverage included in aggregate %.

395 of 483 new or added lines in 14 files covered. (81.78%)

9 existing lines in 2 files now uncovered.

20263 of 26548 relevant lines covered (76.33%)

4347.15 hits per line

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

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

5
import {Buffer, type Device, type RenderPass} from '@luma.gl/core';
6
import {GPUData} from '@luma.gl/tables';
7

8
const UINT32_BYTE_LENGTH = Uint32Array.BYTES_PER_ELEMENT;
8✔
9
const DRAW_RECORD_WORDS = 4;
8✔
10
const DRAW_INDEXED_RECORD_WORDS = 5;
8✔
11

12
export type DrawCommand = {
13
  vertexCount: number;
14
  instanceCount?: number;
15
  firstVertex?: number;
16
  firstInstance?: number;
17
};
18

19
export type DrawIndexedCommand = {
20
  indexCount: number;
21
  instanceCount?: number;
22
  firstIndex?: number;
23
  baseVertex?: number;
24
  firstInstance?: number;
25
};
26

27
export type DrawCommandBufferProps = {
28
  id?: string;
29
  type: 'draw' | 'draw-indexed';
30
  capacity?: number;
31
  commands?: DrawCommand[] | DrawIndexedCommand[];
32
  buffer?: Buffer;
33
  ownsBuffer?: boolean;
34
};
35

36
/** Typed owner or borrower of WebGPU indirect draw records. */
37
export class DrawCommandBuffer {
38
  readonly device: Device;
39
  readonly id: string;
40
  readonly type: 'draw' | 'draw-indexed';
41
  readonly capacity: number;
42
  readonly recordByteLength: number;
43
  readonly buffer: Buffer;
44
  private ownsBuffer: boolean;
45
  private destroyed = false;
2✔
46

47
  constructor(device: Device, props: DrawCommandBufferProps) {
48
    if (device.type !== 'webgpu') {
2!
NEW
49
      throw new Error('DrawCommandBuffer requires a WebGPU device');
×
50
    }
51
    const commands = props.commands ?? [];
2!
52
    const capacity = props.capacity ?? commands.length;
2✔
53
    if (!Number.isSafeInteger(capacity) || capacity < 1) {
2!
NEW
54
      throw new Error('DrawCommandBuffer capacity must be a positive safe integer');
×
55
    }
56
    if (commands.length > capacity) {
2!
NEW
57
      throw new Error('DrawCommandBuffer commands exceed capacity');
×
58
    }
59

60
    this.device = device;
2✔
61
    this.id = props.id ?? 'draw-command-buffer';
2✔
62
    this.type = props.type;
2✔
63
    this.capacity = capacity;
2✔
64
    this.recordByteLength =
2✔
65
      (props.type === 'draw-indexed' ? DRAW_INDEXED_RECORD_WORDS : DRAW_RECORD_WORDS) *
2!
66
      UINT32_BYTE_LENGTH;
67
    const byteLength = capacity * this.recordByteLength;
2✔
68
    const requiredUsage = Buffer.STORAGE | Buffer.INDIRECT | Buffer.COPY_DST | Buffer.COPY_SRC;
2✔
69

70
    if (props.buffer) {
2!
NEW
71
      if (props.buffer.device !== device) {
×
NEW
72
        throw new Error('DrawCommandBuffer buffer must belong to the supplied device');
×
73
      }
NEW
74
      if (props.buffer.byteLength < byteLength) {
×
NEW
75
        throw new Error('DrawCommandBuffer buffer is smaller than capacity');
×
76
      }
NEW
77
      if ((props.buffer.usage & requiredUsage) !== requiredUsage) {
×
NEW
78
        throw new Error(
×
79
          'DrawCommandBuffer buffer requires STORAGE, INDIRECT, COPY_DST, and COPY_SRC usage'
80
        );
81
      }
NEW
82
      this.buffer = props.buffer;
×
NEW
83
      this.ownsBuffer = props.ownsBuffer ?? false;
×
NEW
84
      if (commands.length > 0) {
×
NEW
85
        this.buffer.write(makeCommandData(props.type, capacity, commands));
×
86
      }
87
    } else {
88
      this.buffer = device.createBuffer({
2✔
89
        id: this.id,
90
        data: makeCommandData(props.type, capacity, commands),
91
        usage: requiredUsage
92
      });
93
      this.ownsBuffer = true;
2✔
94
    }
95
  }
96

97
  getCommandByteOffset(commandIndex: number): number {
98
    this.validateCommandIndex(commandIndex);
3✔
99
    return commandIndex * this.recordByteLength;
3✔
100
  }
101

102
  getInstanceCountByteOffset(commandIndex: number): number {
103
    return this.getCommandByteOffset(commandIndex) + UINT32_BYTE_LENGTH;
2✔
104
  }
105

106
  /** Returns a borrowed table view over one command's GPU-written instance count. */
107
  getInstanceCountData(commandIndex: number): GPUData<'uint32'> {
108
    return new GPUData({
1✔
109
      buffer: this.buffer,
110
      format: 'uint32',
111
      length: 1,
112
      byteOffset: this.getInstanceCountByteOffset(commandIndex),
113
      byteStride: UINT32_BYTE_LENGTH,
114
      rowByteLength: UINT32_BYTE_LENGTH,
115
      ownsBuffer: false
116
    });
117
  }
118

119
  /** Records one indirect draw from this buffer. */
120
  draw(renderPass: RenderPass, commandIndex: number): void {
121
    const byteOffset = this.getCommandByteOffset(commandIndex);
1✔
122
    if (this.type === 'draw-indexed') {
1!
NEW
123
      renderPass.drawIndexedIndirect(this.buffer, byteOffset);
×
124
    } else {
125
      renderPass.drawIndirect(this.buffer, byteOffset);
1✔
126
    }
127
  }
128

129
  /** Releases the backing buffer only when this wrapper owns it. */
130
  destroy(): void {
131
    if (this.destroyed) {
2!
NEW
132
      return;
×
133
    }
134
    if (this.ownsBuffer) {
2!
135
      this.buffer.destroy();
2✔
136
      this.ownsBuffer = false;
2✔
137
    }
138
    this.destroyed = true;
2✔
139
  }
140

141
  private validateCommandIndex(commandIndex: number): void {
142
    if (!Number.isSafeInteger(commandIndex) || commandIndex < 0 || commandIndex >= this.capacity) {
3!
NEW
143
      throw new Error(`DrawCommandBuffer command index ${commandIndex} is out of range`);
×
144
    }
145
  }
146
}
147

148
function makeCommandData(
149
  type: 'draw' | 'draw-indexed',
150
  capacity: number,
151
  commands: DrawCommand[] | DrawIndexedCommand[]
152
): Uint32Array {
153
  const recordByteLength =
2✔
154
    (type === 'draw-indexed' ? DRAW_INDEXED_RECORD_WORDS : DRAW_RECORD_WORDS) * UINT32_BYTE_LENGTH;
2!
155
  const data = new ArrayBuffer(capacity * recordByteLength);
2✔
156
  const view = new DataView(data);
2✔
157
  commands.forEach((command, commandIndex) => {
2✔
158
    const byteOffset = commandIndex * recordByteLength;
2✔
159
    if (type === 'draw-indexed') {
2!
NEW
160
      const indexedCommand = command as DrawIndexedCommand;
×
NEW
161
      setUint32(view, byteOffset, indexedCommand.indexCount, 'indexCount');
×
NEW
162
      setUint32(view, byteOffset + 4, indexedCommand.instanceCount ?? 1, 'instanceCount');
×
NEW
163
      setUint32(view, byteOffset + 8, indexedCommand.firstIndex ?? 0, 'firstIndex');
×
NEW
164
      setInt32(view, byteOffset + 12, indexedCommand.baseVertex ?? 0, 'baseVertex');
×
NEW
165
      setUint32(view, byteOffset + 16, indexedCommand.firstInstance ?? 0, 'firstInstance');
×
166
    } else {
167
      const drawCommand = command as DrawCommand;
2✔
168
      setUint32(view, byteOffset, drawCommand.vertexCount, 'vertexCount');
2✔
169
      setUint32(view, byteOffset + 4, drawCommand.instanceCount ?? 1, 'instanceCount');
2!
170
      setUint32(view, byteOffset + 8, drawCommand.firstVertex ?? 0, 'firstVertex');
2✔
171
      setUint32(view, byteOffset + 12, drawCommand.firstInstance ?? 0, 'firstInstance');
2✔
172
    }
173
  });
174
  return new Uint32Array(data);
2✔
175
}
176

177
function setUint32(view: DataView, byteOffset: number, value: number, name: string): void {
178
  if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
8!
NEW
179
    throw new Error(`DrawCommandBuffer ${name} must be a uint32 value`);
×
180
  }
181
  view.setUint32(byteOffset, value, true);
8✔
182
}
183

184
function setInt32(view: DataView, byteOffset: number, value: number, name: string): void {
NEW
185
  if (!Number.isSafeInteger(value) || value < -0x80000000 || value > 0x7fffffff) {
×
NEW
186
    throw new Error(`DrawCommandBuffer ${name} must be an int32 value`);
×
187
  }
NEW
188
  view.setInt32(byteOffset, value, true);
×
189
}
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