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

visgl / luma.gl / 23384903092

21 Mar 2026 05:30PM UTC coverage: 72.417% (+9.3%) from 63.136%
23384903092

push

github

web-flow
feat(devtool); Enable LLMs to automatically test website, package for devtools upstream (#2557)

4103 of 6350 branches covered (64.61%)

Branch coverage included in aggregate %.

215 of 271 new or added lines in 13 files covered. (79.34%)

45 existing lines in 6 files now uncovered.

9124 of 11915 relevant lines covered (76.58%)

277.34 hits per line

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

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

5
import {
6
  type CommandBufferProps,
7
  type CopyBufferToBufferOptions,
8
  type CopyBufferToTextureOptions,
9
  type CopyTextureToBufferOptions,
10
  type CopyTextureToTextureOptions,
11
  // type ClearTextureOptions,
12
  // type TextureReadOptions
13
  CommandBuffer,
14
  Texture,
15
  Framebuffer,
16
  assertDefined
17
} from '@luma.gl/core';
18
import {GL, type GLTextureTarget, type GLTextureCubeMapTarget} from '@luma.gl/constants';
19

20
import {WebGLDevice} from '../webgl-device';
21
import {WEBGLBuffer} from './webgl-buffer';
22
import {WEBGLTexture} from './webgl-texture';
23
import {WEBGLFramebuffer} from './webgl-framebuffer';
24
import {getTextureFormatWebGL} from '../converters/webgl-texture-table';
25

26
type CopyBufferToBufferCommand = {
27
  name: 'copy-buffer-to-buffer';
28
  options: CopyBufferToBufferOptions;
29
};
30

31
type CopyBufferToTextureCommand = {
32
  name: 'copy-buffer-to-texture';
33
  options: CopyBufferToTextureOptions;
34
};
35

36
type CopyTextureToBufferCommand = {
37
  name: 'copy-texture-to-buffer';
38
  options: CopyTextureToBufferOptions;
39
};
40

41
type CopyTextureToTextureCommand = {
42
  name: 'copy-texture-to-texture';
43
  options: CopyTextureToTextureOptions;
44
};
45

46
type ClearTextureCommand = {
47
  name: 'clear-texture';
48
  options: {}; // ClearTextureOptions;
49
};
50

51
type ReadTextureCommand = {
52
  name: 'read-texture';
53
  options: {}; // TextureReadOptions;
54
};
55

56
type Command =
57
  | CopyBufferToBufferCommand
58
  | CopyBufferToTextureCommand
59
  | CopyTextureToBufferCommand
60
  | CopyTextureToTextureCommand
61
  | ClearTextureCommand
62
  | ReadTextureCommand;
63

64
export class WEBGLCommandBuffer extends CommandBuffer {
65
  readonly device: WebGLDevice;
66
  readonly handle = null;
165✔
67
  commands: Command[] = [];
165✔
68

69
  constructor(device: WebGLDevice, props: CommandBufferProps = {}) {
165✔
70
    super(device, props);
165✔
71
    this.device = device;
165✔
72
  }
73

74
  _executeCommands(commands: Command[] = this.commands) {
53✔
75
    for (const command of commands) {
53✔
76
      switch (command.name) {
3!
77
        case 'copy-buffer-to-buffer':
UNCOV
78
          _copyBufferToBuffer(this.device, command.options);
×
UNCOV
79
          break;
×
80
        case 'copy-buffer-to-texture':
81
          _copyBufferToTexture(this.device, command.options);
×
82
          break;
×
83
        case 'copy-texture-to-buffer':
84
          _copyTextureToBuffer(this.device, command.options);
3✔
85
          break;
3✔
86
        case 'copy-texture-to-texture':
UNCOV
87
          _copyTextureToTexture(this.device, command.options);
×
UNCOV
88
          break;
×
89
        // case 'clear-texture':
90
        //   _clearTexture(this.device, command.options);
91
        //   break;
92
        default:
93
          throw new Error(command.name);
×
94
      }
95
    }
96
  }
97
}
98

99
function _copyBufferToBuffer(device: WebGLDevice, options: CopyBufferToBufferOptions): void {
UNCOV
100
  const source = options.sourceBuffer as WEBGLBuffer;
×
UNCOV
101
  const destination = options.destinationBuffer as WEBGLBuffer;
×
102

103
  // {In WebGL2 we can p}erform the copy on the GPU
104
  // Use GL.COPY_READ_BUFFER+GL.COPY_WRITE_BUFFER avoid disturbing other targets and locking type
UNCOV
105
  device.gl.bindBuffer(GL.COPY_READ_BUFFER, source.handle);
×
UNCOV
106
  device.gl.bindBuffer(GL.COPY_WRITE_BUFFER, destination.handle);
×
UNCOV
107
  device.gl.copyBufferSubData(
×
108
    GL.COPY_READ_BUFFER,
109
    GL.COPY_WRITE_BUFFER,
110
    options.sourceOffset ?? 0,
×
111
    options.destinationOffset ?? 0,
×
112
    options.size
113
  );
UNCOV
114
  device.gl.bindBuffer(GL.COPY_READ_BUFFER, null);
×
UNCOV
115
  device.gl.bindBuffer(GL.COPY_WRITE_BUFFER, null);
×
116
}
117

118
/**
119
 * Copies data from a Buffer object into a Texture object
120
 * NOTE: doesn't wait for copy to be complete
121
 */
122
function _copyBufferToTexture(device: WebGLDevice, options: CopyBufferToTextureOptions): void {
123
  throw new Error('Not implemented');
×
124
}
125

126
/**
127
 * Copies data from a Texture object into a Buffer object.
128
 * NOTE: doesn't wait for copy to be complete
129
 */
130
function _copyTextureToBuffer(device: WebGLDevice, options: CopyTextureToBufferOptions): void {
131
  const {
132
    /** Texture to copy to/from. */
133
    sourceTexture,
134
    /**  Mip-map level of the texture to copy to/from. (Default 0) */
135
    mipLevel = 0,
3✔
136
    /** Defines which aspects of the texture to copy to/from. */
137
    aspect = 'all',
3✔
138

139
    /** Width to copy */
140
    width = options.sourceTexture.width,
3✔
141
    /** Height to copy */
142
    height = options.sourceTexture.height,
3✔
143
    depthOrArrayLayers = 0,
3✔
144
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to/from. */
145
    origin = [0, 0, 0],
3✔
146

147
    /** Destination buffer */
148
    destinationBuffer,
149
    /** Offset, in bytes, from the beginning of the buffer to the start of the image data (default 0) */
150
    byteOffset = 0,
3✔
151
    /**
152
     * The stride, in bytes, between the beginning of each block row and the subsequent block row.
153
     * Required if there are multiple block rows (i.e. the copy height or depth is more than one block).
154
     */
155
    bytesPerRow,
156
    /**
157
     * Number of block rows per single image of the texture.
158
     * rowsPerImage × bytesPerRow is the stride, in bytes, between the beginning of each image of data and the subsequent image.
159
     * Required if there are multiple images (i.e. the copy depth is more than one).
160
     */
161
    rowsPerImage
162
  } = options;
3✔
163

164
  // TODO - Not possible to read just stencil or depth part in WebGL?
165
  if (aspect !== 'all') {
3!
166
    throw new Error('aspect not supported in WebGL');
×
167
  }
168

169
  // TODO - mipLevels are set when attaching texture to framebuffer
170
  if (mipLevel !== 0 || depthOrArrayLayers !== 0 || bytesPerRow || rowsPerImage) {
3!
171
    throw new Error('not implemented');
×
172
  }
173

174
  // Asynchronous read (PIXEL_PACK_BUFFER) is WebGL2 only feature
175
  const {framebuffer, destroyFramebuffer} = getFramebuffer(sourceTexture);
3✔
176
  let prevHandle: WebGLFramebuffer | null | undefined;
177
  try {
3✔
178
    const webglBuffer = destinationBuffer as WEBGLBuffer;
3✔
179
    const sourceWidth = width || framebuffer.width;
3!
180
    const sourceHeight = height || framebuffer.height;
3!
181
    const colorAttachment0 = assertDefined(framebuffer.colorAttachments[0]);
3✔
182

183
    const sourceParams = getTextureFormatWebGL(colorAttachment0.texture.props.format);
3✔
184
    const sourceFormat = sourceParams.format;
3✔
185
    const sourceType = sourceParams.type;
3✔
186

187
    // if (!target) {
188
    //   // Create new buffer with enough size
189
    //   const components = glFormatToComponents(sourceFormat);
190
    //   const byteCount = glTypeToBytes(sourceType);
191
    //   const byteLength = byteOffset + sourceWidth * sourceHeight * components * byteCount;
192
    //   target = device.createBuffer({byteLength});
193
    // }
194

195
    device.gl.bindBuffer(GL.PIXEL_PACK_BUFFER, webglBuffer.handle);
3✔
196
    // @ts-expect-error native bindFramebuffer is overridden by our state tracker
197
    prevHandle = device.gl.bindFramebuffer(GL.FRAMEBUFFER, framebuffer.handle);
3✔
198

199
    device.gl.readPixels(
3✔
200
      origin[0],
201
      origin[1],
202
      sourceWidth,
203
      sourceHeight,
204
      sourceFormat,
205
      sourceType,
206
      byteOffset
207
    );
208
  } finally {
209
    device.gl.bindBuffer(GL.PIXEL_PACK_BUFFER, null);
3✔
210
    // prevHandle may be unassigned if the try block failed before binding
211
    if (prevHandle !== undefined) {
3!
212
      device.gl.bindFramebuffer(GL.FRAMEBUFFER, prevHandle);
3✔
213
    }
214

215
    if (destroyFramebuffer) {
3!
216
      framebuffer.destroy();
3✔
217
    }
218
  }
219
}
220

221
/**
222
 * Copies data from a Framebuffer or a Texture object into a Buffer object.
223
 * NOTE: doesn't wait for copy to be complete, it programs GPU to perform a DMA transfer.
224
export function readPixelsToBuffer(
225
  source: Framebuffer | Texture,
226
  options?: {
227
    sourceX?: number;
228
    sourceY?: number;
229
    sourceFormat?: number;
230
    target?: Buffer; // A new Buffer object is created when not provided.
231
    targetByteOffset?: number; // byte offset in buffer object
232
    // following parameters are auto deduced if not provided
233
    sourceWidth?: number;
234
    sourceHeight?: number;
235
    sourceType?: number;
236
  }
237
): Buffer
238
 */
239

240
/**
241
 * Copy a rectangle from a Framebuffer or Texture object into a texture (at an offset)
242
 */
243
// eslint-disable-next-line complexity, max-statements
244
function _copyTextureToTexture(device: WebGLDevice, options: CopyTextureToTextureOptions): void {
245
  const {
246
    /** Texture to copy to/from. */
247
    sourceTexture,
248
    /**  Mip-map level of the texture to copy to (Default 0) */
249
    destinationMipLevel = 0,
×
250
    /** Defines which aspects of the texture to copy to/from. */
251
    // aspect = 'all',
252
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy from. */
253
    origin = [0, 0],
×
254

255
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to. */
256
    destinationOrigin = [0, 0, 0],
×
257

258
    /** Texture to copy to/from. */
259
    destinationTexture
260
    /**  Mip-map level of the texture to copy to/from. (Default 0) */
261
    // destinationMipLevel = options.mipLevel,
262
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to/from. */
263
    // destinationOrigin = [0, 0],
264
    /** Defines which aspects of the texture to copy to/from. */
265
    // destinationAspect = options.aspect,
UNCOV
266
  } = options;
×
267

268
  let {
269
    width = options.destinationTexture.width,
×
270
    height = options.destinationTexture.height
×
271
    // depthOrArrayLayers = 0
UNCOV
272
  } = options;
×
273

UNCOV
274
  const {framebuffer, destroyFramebuffer} = getFramebuffer(sourceTexture);
×
UNCOV
275
  const [sourceX = 0, sourceY = 0] = origin;
×
UNCOV
276
  const [destinationX, destinationY, destinationZ] = destinationOrigin;
×
277

278
  // @ts-expect-error native bindFramebuffer is overridden by our state tracker
UNCOV
279
  const prevHandle: WebGLFramebuffer | null = device.gl.bindFramebuffer(
×
280
    GL.FRAMEBUFFER,
281
    framebuffer.handle
282
  );
283
  // TODO - support gl.readBuffer (WebGL2 only)
284
  // const prevBuffer = gl.readBuffer(attachment);
285

286
  let texture: WEBGLTexture;
287
  let textureTarget: GL;
UNCOV
288
  if (destinationTexture instanceof WEBGLTexture) {
×
UNCOV
289
    texture = destinationTexture;
×
UNCOV
290
    width = Number.isFinite(width) ? width : texture.width;
×
UNCOV
291
    height = Number.isFinite(height) ? height : texture.height;
×
UNCOV
292
    texture._bind(0);
×
UNCOV
293
    textureTarget = texture.glTarget;
×
294
  } else {
295
    throw new Error('invalid destination');
×
296
  }
297

UNCOV
298
  switch (textureTarget) {
×
299
    case GL.TEXTURE_2D:
300
    case GL.TEXTURE_CUBE_MAP:
UNCOV
301
      device.gl.copyTexSubImage2D(
×
302
        textureTarget,
303
        destinationMipLevel,
304
        destinationX,
305
        destinationY,
306
        sourceX,
307
        sourceY,
308
        width,
309
        height
310
      );
UNCOV
311
      break;
×
312
    case GL.TEXTURE_2D_ARRAY:
313
    case GL.TEXTURE_3D:
314
      device.gl.copyTexSubImage3D(
×
315
        textureTarget,
316
        destinationMipLevel,
317
        destinationX,
318
        destinationY,
319
        destinationZ,
320
        sourceX,
321
        sourceY,
322
        width,
323
        height
324
      );
325
      break;
×
326
    default:
327
  }
328

UNCOV
329
  if (texture) {
×
UNCOV
330
    texture._unbind();
×
331
  }
UNCOV
332
  device.gl.bindFramebuffer(GL.FRAMEBUFFER, prevHandle);
×
UNCOV
333
  if (destroyFramebuffer) {
×
UNCOV
334
    framebuffer.destroy();
×
335
  }
336
}
337

338
/** Clear one mip level of a texture *
339
function _clearTexture(device: WebGLDevice, options: ClearTextureOptions) {
340
  const BORDER = 0;
341
  const {dimension, width, height, depth = 0, mipLevel = 0} = options;
342
  const {glInternalFormat, glFormat, glType, compressed} = options;
343
  const glTarget = getWebGLCubeFaceTarget(options.glTarget, dimension, depth);
344

345
  switch (dimension) {
346
    case '2d-array':
347
    case '3d':
348
      if (compressed) {
349
        // prettier-ignore
350
        device.gl.compressedTexImage3D(glTarget, mipLevel, glInternalFormat, width, height, depth, BORDER, null);
351
      } else {
352
        // prettier-ignore
353
        device.gl.texImage3D( glTarget, mipLevel, glInternalFormat, width, height, depth, BORDER, glFormat, glType, null);
354
      }
355
      break;
356

357
    case '2d':
358
    case 'cube':
359
      if (compressed) {
360
        // prettier-ignore
361
        device.gl.compressedTexImage2D(glTarget, mipLevel, glInternalFormat, width, height, BORDER, null);
362
      } else {
363
        // prettier-ignore
364
        device.gl.texImage2D(glTarget, mipLevel, glInternalFormat, width, height, BORDER, glFormat, glType, null);
365
      }
366
      break;
367

368
    default:
369
      throw new Error(dimension);
370
  }
371
}
372
  */
373

374
// function _readTexture(device: WebGLDevice, options: CopyTextureToBufferOptions) {}
375

376
// HELPERS
377

378
/**
379
 * In WebGL, cube maps specify faces by overriding target instead of using the depth parameter.
380
 * @note We still bind the texture using GL.TEXTURE_CUBE_MAP, but we need to use the face-specific target when setting mip levels.
381
 * @returns glTarget unchanged, if dimension !== 'cube'.
382
 */
383
export function getWebGLCubeFaceTarget(
384
  glTarget: GLTextureTarget,
385
  dimension: '1d' | '2d' | '2d-array' | 'cube' | 'cube-array' | '3d',
386
  level: number
387
): GLTextureTarget | GLTextureCubeMapTarget {
388
  return dimension === 'cube' ? GL.TEXTURE_CUBE_MAP_POSITIVE_X + level : glTarget;
×
389
}
390

391
/** Wrap a texture in a framebuffer so that we can use WebGL APIs that work on framebuffers */
392
function getFramebuffer(source: Texture | Framebuffer): {
393
  framebuffer: WEBGLFramebuffer;
394
  destroyFramebuffer: boolean;
395
} {
396
  if (source instanceof Texture) {
3!
397
    const {width, height, id} = source;
3✔
398
    const framebuffer = source.device.createFramebuffer({
3✔
399
      id: `framebuffer-for-${id}`,
400
      width,
401
      height,
402
      colorAttachments: [source]
403
    }) as unknown as WEBGLFramebuffer;
404

405
    return {framebuffer, destroyFramebuffer: true};
3✔
406
  }
UNCOV
407
  return {framebuffer: source as unknown as WEBGLFramebuffer, destroyFramebuffer: false};
×
408
}
409

410
/**
411
 * Returns number of components in a specific readPixels WebGL format
412
 * @todo use shadertypes utils instead?
413
 */
414
export function glFormatToComponents(format: GL): 1 | 2 | 3 | 4 {
415
  switch (format) {
×
416
    case GL.ALPHA:
417
    case GL.R32F:
418
    case GL.RED:
419
      return 1;
×
420
    case GL.RG32F:
421
    case GL.RG:
422
      return 2;
×
423
    case GL.RGB:
424
    case GL.RGB32F:
425
      return 3;
×
426
    case GL.RGBA:
427
    case GL.RGBA32F:
428
      return 4;
×
429
    // TODO: Add support for additional WebGL2 formats
430
    default:
431
      throw new Error('GLFormat');
×
432
  }
433
}
434

435
/**
436
 * Return byte count for given readPixels WebGL type
437
 * @todo use shadertypes utils instead?
438
 */
439
export function glTypeToBytes(type: GL): 1 | 2 | 4 {
440
  switch (type) {
×
441
    case GL.UNSIGNED_BYTE:
442
      return 1;
×
443
    case GL.UNSIGNED_SHORT_5_6_5:
444
    case GL.UNSIGNED_SHORT_4_4_4_4:
445
    case GL.UNSIGNED_SHORT_5_5_5_1:
446
      return 2;
×
447
    case GL.FLOAT:
448
      return 4;
×
449
    // TODO: Add support for additional WebGL2 types
450
    default:
451
      throw new Error('GLType');
×
452
  }
453
}
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