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

visgl / luma.gl / 27877205100

20 Jun 2026 04:32PM UTC coverage: 70.733% (+0.08%) from 70.652%
27877205100

push

github

web-flow
feat(engine) add portable video textures (#2677)

9660 of 15389 branches covered (62.77%)

Branch coverage included in aggregate %.

99 of 116 new or added lines in 2 files covered. (85.34%)

379 existing lines in 28 files now uncovered.

19717 of 26143 relevant lines covered (75.42%)

4107.12 hits per line

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

85.8
/modules/webgpu/src/adapter/resources/webgpu-buffer.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {log, Buffer, type BufferProps, type BufferMapCallback} from '@luma.gl/core';
6
import {type WebGPUDevice} from '../webgpu-device';
7

8
/**
9
 * WebGPU implementation of Buffer
10
 * For byte alignment requirements see:
11
 * @see https://www.w3.org/TR/webgpu/#dom-gpubuffer-mapasync
12
 * @see https://developer.mozilla.org/en-US/docs/Web/API/GPUBuffer/mapAsync
13
 */
14
export class WebGPUBuffer extends Buffer {
15
  readonly device: WebGPUDevice;
16
  readonly handle: GPUBuffer;
17
  readonly byteLength: number;
18
  readonly paddedByteLength: number;
19

20
  constructor(device: WebGPUDevice, props: BufferProps) {
21
    super(device, props);
878✔
22
    this.device = device;
878✔
23

24
    this.byteLength = props.byteLength || props.data?.byteLength || 0;
878✔
25
    this.paddedByteLength = Math.ceil(this.byteLength / 4) * 4;
878✔
26
    const mappedAtCreation = Boolean(this.props.onMapped || props.data);
878✔
27

28
    // WebGPU buffers must be aligned to 4 bytes
29
    const size = this.paddedByteLength;
878✔
30

31
    this.device.pushErrorScope('out-of-memory');
878✔
32
    this.device.pushErrorScope('validation');
878✔
33
    const suppliedHandle = this.props.handle as GPUBuffer | undefined;
878✔
34
    this.handle =
878✔
35
      suppliedHandle ||
1,755✔
36
      this.device.handle.createBuffer({
37
        label: this.props.id,
38
        // usage defaults to vertex
39
        usage: this.props.usage || GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
916✔
40
        mappedAtCreation,
41
        size
42
      });
43
    this.device.popErrorScope((error: GPUError) => {
878✔
44
      this.device.reportError(new Error(`${this} creation failed ${error.message}`), this)();
×
UNCOV
45
      this.device.debug();
×
46
    });
47
    this.device.popErrorScope((error: GPUError) => {
878✔
48
      this.device.reportError(new Error(`${this} out of memory: ${error.message}`), this)();
×
UNCOV
49
      this.device.debug();
×
50
    });
51

52
    this.device.pushErrorScope('validation');
878✔
53
    if (props.data || props.onMapped) {
878✔
54
      try {
585✔
55
        const arrayBuffer = this.handle.getMappedRange();
585✔
56
        if (props.data) {
585!
57
          const typedArray = props.data;
585✔
58
          // @ts-expect-error
59
          new typedArray.constructor(arrayBuffer).set(typedArray);
585✔
60
        } else {
UNCOV
61
          props.onMapped?.(arrayBuffer, 'mapped');
×
62
        }
63
      } finally {
64
        this.handle.unmap();
585✔
65
      }
66
    }
67
    this.device.popErrorScope((error: GPUError) => {
878✔
68
      this.device.reportError(new Error(`${this} creation failed ${error.message}`), this)();
×
UNCOV
69
      this.device.debug();
×
70
    });
71

72
    if (!this.props.handle) {
878✔
73
      this.trackAllocatedMemory(size);
877✔
74
    } else {
75
      this.trackReferencedMemory(size, 'Buffer');
1✔
76
    }
77
  }
78

79
  override destroy(): void {
80
    if (!this.destroyed && this.handle) {
823✔
81
      this.removeStats();
822✔
82
      if (!this.props.handle) {
822✔
83
        this.trackDeallocatedMemory();
821✔
84
        this.handle.destroy();
821✔
85
      } else {
86
        this.trackDeallocatedReferencedMemory('Buffer');
1✔
87
      }
88
      this.destroyed = true;
822✔
89
      // @ts-expect-error readonly
90
      this.handle = null;
822✔
91
    }
92
  }
93

94
  write(data: ArrayBufferLike | ArrayBufferView | SharedArrayBuffer, byteOffset = 0) {
131✔
95
    const arrayBuffer = ArrayBuffer.isView(data) ? data.buffer : data;
131!
96
    const dataByteOffset = ArrayBuffer.isView(data) ? data.byteOffset : 0;
131!
97

98
    this.device.pushErrorScope('validation');
131✔
99

100
    // WebGPU provides multiple ways to write a buffer, this is the simplest API
101
    this.device.handle.queue.writeBuffer(
131✔
102
      this.handle,
103
      byteOffset,
104
      arrayBuffer,
105
      dataByteOffset,
106
      data.byteLength
107
    );
108
    this.device.popErrorScope((error: GPUError) => {
131✔
109
      this.device.reportError(new Error(`${this}.write() ${error.message}`), this)();
×
UNCOV
110
      this.device.debug();
×
111
    });
112
  }
113

114
  async mapAndWriteAsync(
115
    callback: BufferMapCallback<void>,
116
    byteOffset: number = 0,
2✔
117
    byteLength: number = this.byteLength - byteOffset
2✔
118
  ): Promise<void> {
119
    const alignedByteLength = Math.ceil(byteLength / 4) * 4;
2✔
120
    // Unless the application created and supplied a mappable buffer, a staging buffer is needed
121
    const isMappable = (this.usage & Buffer.MAP_WRITE) !== 0;
2✔
122
    const mappableBuffer: WebGPUBuffer | null = !isMappable
2!
123
      ? this._getMappableBuffer(Buffer.MAP_WRITE | Buffer.COPY_SRC, 0, this.paddedByteLength)
124
      : null;
125

126
    const writeBuffer = mappableBuffer || this;
2!
127

128
    // const isWritable = this.usage & Buffer.MAP_WRITE;
129
    // Map the temp buffer and read the data.
130
    this.device.pushErrorScope('validation');
2✔
131
    try {
2✔
132
      await this.device.handle.queue.onSubmittedWorkDone();
2✔
133
      await writeBuffer.handle.mapAsync(GPUMapMode.WRITE, byteOffset, alignedByteLength);
2✔
134
      const mappedRange = writeBuffer.handle.getMappedRange(byteOffset, alignedByteLength);
2✔
135
      const arrayBuffer = mappedRange.slice(0, byteLength);
2✔
136
      // eslint-disable-next-line @typescript-eslint/await-thenable
137
      await callback(arrayBuffer, 'mapped');
2✔
138
      new Uint8Array(mappedRange).set(new Uint8Array(arrayBuffer), 0);
2✔
139
      writeBuffer.handle.unmap();
2✔
140
      if (mappableBuffer) {
2!
141
        this._copyBuffer(mappableBuffer, byteOffset, alignedByteLength);
2✔
142
      }
143
    } finally {
144
      this.device.popErrorScope((error: GPUError) => {
2✔
145
        this.device.reportError(new Error(`${this}.mapAndWriteAsync() ${error.message}`), this)();
×
UNCOV
146
        this.device.debug();
×
147
      });
148
      mappableBuffer?.destroy();
2✔
149
    }
150
  }
151

152
  async readAsync(
153
    byteOffset: number = 0,
159✔
154
    byteLength = this.byteLength - byteOffset
159✔
155
  ): Promise<Uint8Array> {
156
    return this.mapAndReadAsync(
159✔
157
      arrayBuffer => new Uint8Array(arrayBuffer.slice(0)),
159✔
158
      byteOffset,
159
      byteLength
160
    );
161
  }
162

163
  async mapAndReadAsync<T>(
164
    callback: BufferMapCallback<T>,
165
    byteOffset = 0,
169✔
166
    byteLength = this.byteLength - byteOffset
169✔
167
  ): Promise<T> {
168
    const requestedEnd = byteOffset + byteLength;
169✔
169
    if (requestedEnd > this.byteLength) {
169✔
170
      throw new Error('Mapping range exceeds buffer size');
1✔
171
    }
172

173
    let mappedByteOffset = byteOffset;
168✔
174
    let mappedByteLength = byteLength;
168✔
175
    let sliceByteOffset = 0;
168✔
176
    let lifetime: 'mapped' | 'copied' = 'mapped';
168✔
177

178
    // WebGPU mapAsync requires 8-byte offsets and 4-byte lengths.
179
    if (byteOffset % 8 !== 0 || byteLength % 4 !== 0) {
168✔
180
      mappedByteOffset = Math.floor(byteOffset / 8) * 8;
8✔
181
      const alignedEnd = Math.ceil(requestedEnd / 4) * 4;
8✔
182
      mappedByteLength = alignedEnd - mappedByteOffset;
8✔
183
      sliceByteOffset = byteOffset - mappedByteOffset;
8✔
184
      lifetime = 'copied';
8✔
185
    }
186

187
    if (mappedByteOffset + mappedByteLength > this.paddedByteLength) {
168!
UNCOV
188
      throw new Error('Mapping range exceeds buffer size');
×
189
    }
190

191
    // Unless the application created and supplied a mappable buffer, a staging buffer is needed
192
    const isMappable = (this.usage & Buffer.MAP_READ) !== 0;
168✔
193
    const mappableBuffer: WebGPUBuffer | null = !isMappable
168✔
194
      ? this._getMappableBuffer(Buffer.MAP_READ | Buffer.COPY_DST, 0, this.paddedByteLength)
195
      : null;
196

197
    const readBuffer = mappableBuffer || this;
169✔
198

199
    // Map the temp buffer and read the data.
200
    this.device.pushErrorScope('validation');
169✔
201
    try {
169✔
202
      await this.device.handle.queue.onSubmittedWorkDone();
169✔
203
      if (mappableBuffer) {
168✔
204
        mappableBuffer._copyBuffer(this, mappedByteOffset, mappedByteLength);
120✔
205
      }
206
      await readBuffer.handle.mapAsync(GPUMapMode.READ, mappedByteOffset, mappedByteLength);
168✔
207
      const arrayBuffer = readBuffer.handle.getMappedRange(mappedByteOffset, mappedByteLength);
168✔
208
      const mappedRange =
209
        lifetime === 'mapped'
168✔
210
          ? arrayBuffer
211
          : arrayBuffer.slice(sliceByteOffset, sliceByteOffset + byteLength);
212
      // eslint-disable-next-line @typescript-eslint/await-thenable
213
      const result = await callback(mappedRange, lifetime);
169✔
214
      readBuffer.handle.unmap();
168✔
215
      return result;
168✔
216
    } finally {
217
      this.device.popErrorScope((error: GPUError) => {
168✔
218
        this.device.reportError(new Error(`${this}.mapAndReadAsync() ${error.message}`), this)();
×
UNCOV
219
        this.device.debug();
×
220
      });
221
      mappableBuffer?.destroy();
168✔
222
    }
223
  }
224

225
  readSyncWebGL(byteOffset?: number, byteLength?: number): Uint8Array<ArrayBuffer> {
UNCOV
226
    throw new Error('Not implemented');
×
227
  }
228

229
  // INTERNAL METHODS
230

231
  /**
232
   * @todo - A small set of mappable buffers could be cached on the device,
233
   * however this goes against the goal of keeping core as a thin GPU API layer.
234
   */
235
  protected _getMappableBuffer(
236
    usage: number, // Buffer.MAP_READ | Buffer.MAP_WRITE,
237
    byteOffset: number,
238
    byteLength: number
239
  ): WebGPUBuffer {
240
    log.warn(`${this} is not readable, creating a temporary Buffer`);
122✔
241
    const readableBuffer = new WebGPUBuffer(this.device, {usage, byteLength});
122✔
242

243
    return readableBuffer;
122✔
244
  }
245

246
  protected _copyBuffer(
247
    sourceBuffer: WebGPUBuffer,
248
    byteOffset: number = 0,
122✔
249
    byteLength: number = this.byteLength
122✔
250
  ) {
251
    // Now do a GPU-side copy into the temp buffer we can actually read.
252
    // TODO - we are spinning up an independent command queue here, what does this mean
253
    this.device.pushErrorScope('validation');
122✔
254
    const commandEncoder = this.device.handle.createCommandEncoder();
122✔
255
    commandEncoder.copyBufferToBuffer(
122✔
256
      sourceBuffer.handle,
257
      byteOffset,
258
      this.handle,
259
      byteOffset,
260
      byteLength
261
    );
262
    this.device.handle.queue.submit([commandEncoder.finish()]);
122✔
263
    this.device.popErrorScope((error: GPUError) => {
122✔
264
      this.device.reportError(new Error(`${this}._getReadableBuffer() ${error.message}`), this)();
×
UNCOV
265
      this.device.debug();
×
266
    });
267
  }
268
}
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