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

visgl / luma.gl / 28177465729

25 Jun 2026 02:28PM UTC coverage: 70.408% (-0.3%) from 70.66%
28177465729

push

github

web-flow
website: Improve example browsing and InfoBox panels (#2692)

9602 of 15462 branches covered (62.1%)

Branch coverage included in aggregate %.

19228 of 25485 relevant lines covered (75.45%)

4245.37 hits per line

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

27.42
/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 ResourceProps,
7
  type CopyBufferToBufferOptions,
8
  type CopyBufferToTextureOptions,
9
  type CopyTextureToBufferOptions,
10
  type CopyTextureToTextureOptions,
11
  type TextureReadOptions,
12
  // type ClearTextureOptions,
13
  CommandBuffer,
14
  Texture,
15
  Framebuffer,
16
  assertDefined
17
} from '@luma.gl/core';
18
import {GL, type GLTextureTarget, type GLTextureCubeMapTarget} from '@luma.gl/webgl/constants';
19

20
import {getTextureFormatWebGL} from '../converters/webgl-texture-table';
21
import {WebGLDevice} from '../webgl-device';
22
import {WEBGLBuffer} from './webgl-buffer';
23
import {WEBGLTexture} from './webgl-texture';
24
import {WEBGLFramebuffer} from './webgl-framebuffer';
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;
260✔
67
  commands: Command[] = [];
260✔
68

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

74
  _executeCommands(commands: Command[] = this.commands) {
71✔
75
    for (const command of commands) {
71✔
76
      switch (command.name) {
14!
77
        case 'copy-buffer-to-buffer':
78
          _copyBufferToBuffer(this.device, command.options);
3✔
79
          break;
3✔
80
        case 'copy-buffer-to-texture':
81
          _copyBufferToTexture(this.device, command.options);
7✔
82
          break;
7✔
83
        case 'copy-texture-to-buffer':
84
          _copyTextureToBuffer(this.device, command.options);
4✔
85
          break;
4✔
86
        case 'copy-texture-to-texture':
87
          _copyTextureToTexture(this.device, command.options);
×
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 {
100
  const source = options.sourceBuffer as WEBGLBuffer;
3✔
101
  const destination = options.destinationBuffer as WEBGLBuffer;
3✔
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
105
  device.gl.bindBuffer(GL.COPY_READ_BUFFER, source.handle);
3✔
106
  device.gl.bindBuffer(GL.COPY_WRITE_BUFFER, destination.handle);
3✔
107
  device.gl.copyBufferSubData(
3✔
108
    GL.COPY_READ_BUFFER,
109
    GL.COPY_WRITE_BUFFER,
110
    options.sourceOffset ?? 0,
6✔
111
    options.destinationOffset ?? 0,
6✔
112
    options.size
113
  );
114
  device.gl.bindBuffer(GL.COPY_READ_BUFFER, null);
3✔
115
  device.gl.bindBuffer(GL.COPY_WRITE_BUFFER, null);
3✔
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
  const {
124
    sourceBuffer,
125
    byteOffset = 0,
×
126
    destinationTexture,
127
    mipLevel = 0,
7✔
128
    origin = [0, 0, 0],
7✔
129
    aspect = 'all',
7✔
130
    bytesPerRow,
131
    rowsPerImage,
132
    size
133
  } = _options;
7✔
134

135
  if (aspect !== 'all') {
7!
136
    throw new Error('copyBufferToTexture aspect is not supported in WebGL');
×
137
  }
138

139
  destinationTexture.writeBuffer(sourceBuffer, {
7✔
140
    byteOffset,
141
    bytesPerRow,
142
    rowsPerImage,
143
    mipLevel,
144
    x: origin[0] ?? 0,
7!
145
    y: origin[1] ?? 0,
7!
146
    z: origin[2] ?? 0,
7!
147
    width: size[0],
148
    height: size[1],
149
    depthOrArrayLayers: size[2]
150
  });
151
}
152

153
/**
154
 * Copies data from a Texture object into a Buffer object.
155
 * NOTE: doesn't wait for copy to be complete
156
 */
157
function _copyTextureToBuffer(device: WebGLDevice, options: CopyTextureToBufferOptions): void {
158
  const {
159
    sourceTexture,
160
    mipLevel = 0,
4✔
161
    aspect = 'all',
4✔
162
    width = options.sourceTexture.width,
3✔
163
    height = options.sourceTexture.height,
3✔
164
    depthOrArrayLayers,
165
    origin = [0, 0, 0],
4✔
166
    destinationBuffer,
167
    byteOffset = 0,
3✔
168
    bytesPerRow,
169
    rowsPerImage
170
  } = options;
4✔
171

172
  if (sourceTexture instanceof Texture) {
4!
173
    sourceTexture.readBuffer(
4✔
174
      {
175
        x: origin[0] ?? 0,
4!
176
        y: origin[1] ?? 0,
4!
177
        z: origin[2] ?? 0,
4!
178
        width,
179
        height,
180
        depthOrArrayLayers,
181
        mipLevel,
182
        aspect,
183
        byteOffset
184
      } as TextureReadOptions & {byteOffset?: number},
185
      destinationBuffer
186
    );
187
    return;
4✔
188
  }
189

190
  // TODO - Not possible to read just stencil or depth part in WebGL?
191
  if (aspect !== 'all') {
×
192
    throw new Error('aspect not supported in WebGL');
×
193
  }
194

195
  // TODO - mipLevels are set when attaching texture to framebuffer
196
  if (mipLevel !== 0 || depthOrArrayLayers !== undefined || bytesPerRow || rowsPerImage) {
×
197
    throw new Error('not implemented');
×
198
  }
199

200
  // Asynchronous read (PIXEL_PACK_BUFFER) is WebGL2 only feature
201
  const {framebuffer, destroyFramebuffer} = getFramebuffer(sourceTexture);
×
202
  let prevHandle: WebGLFramebuffer | null | undefined;
203
  try {
×
204
    const webglBuffer = destinationBuffer as WEBGLBuffer;
×
205
    const sourceWidth = width || framebuffer.width;
×
206
    const sourceHeight = height || framebuffer.height;
×
207
    const colorAttachment0 = assertDefined(framebuffer.colorAttachments[0]);
×
208

209
    const sourceParams = getTextureFormatWebGL(colorAttachment0.texture.props.format);
×
210
    const sourceFormat = sourceParams.format;
×
211
    const sourceType = sourceParams.type;
×
212

213
    // if (!target) {
214
    //   // Create new buffer with enough size
215
    //   const components = glFormatToComponents(sourceFormat);
216
    //   const byteCount = glTypeToBytes(sourceType);
217
    //   const byteLength = byteOffset + sourceWidth * sourceHeight * components * byteCount;
218
    //   target = device.createBuffer({byteLength});
219
    // }
220

221
    device.gl.bindBuffer(GL.PIXEL_PACK_BUFFER, webglBuffer.handle);
×
222
    // @ts-expect-error native bindFramebuffer is overridden by our state tracker
223
    prevHandle = device.gl.bindFramebuffer(GL.FRAMEBUFFER, framebuffer.handle);
×
224

225
    device.gl.readPixels(
×
226
      origin[0],
227
      origin[1],
228
      sourceWidth,
229
      sourceHeight,
230
      sourceFormat,
231
      sourceType,
232
      byteOffset
233
    );
234
  } finally {
235
    device.gl.bindBuffer(GL.PIXEL_PACK_BUFFER, null);
×
236
    // prevHandle may be unassigned if the try block failed before binding
237
    if (prevHandle !== undefined) {
×
238
      device.gl.bindFramebuffer(GL.FRAMEBUFFER, prevHandle);
×
239
    }
240

241
    if (destroyFramebuffer) {
×
242
      framebuffer.destroy();
×
243
    }
244
  }
245
}
246

247
/**
248
 * Copies data from a Framebuffer or a Texture object into a Buffer object.
249
 * NOTE: doesn't wait for copy to be complete, it programs GPU to perform a DMA transfer.
250
export function readPixelsToBuffer(
251
  source: Framebuffer | Texture,
252
  options?: {
253
    sourceX?: number;
254
    sourceY?: number;
255
    sourceFormat?: number;
256
    target?: Buffer; // A new Buffer object is created when not provided.
257
    targetByteOffset?: number; // byte offset in buffer object
258
    // following parameters are auto deduced if not provided
259
    sourceWidth?: number;
260
    sourceHeight?: number;
261
    sourceType?: number;
262
  }
263
): Buffer
264
 */
265

266
/**
267
 * Copy a rectangle from a Framebuffer or Texture object into a texture (at an offset)
268
 */
269
// eslint-disable-next-line complexity, max-statements
270
function _copyTextureToTexture(device: WebGLDevice, options: CopyTextureToTextureOptions): void {
271
  const {
272
    /** Texture to copy to/from. */
273
    sourceTexture,
274
    /**  Mip-map level of the texture to copy to (Default 0) */
275
    destinationMipLevel = 0,
×
276
    /** Defines which aspects of the texture to copy to/from. */
277
    // aspect = 'all',
278
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy from. */
279
    origin = [0, 0],
×
280

281
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to. */
282
    destinationOrigin = [0, 0, 0],
×
283

284
    /** Texture to copy to/from. */
285
    destinationTexture
286
    /**  Mip-map level of the texture to copy to/from. (Default 0) */
287
    // destinationMipLevel = options.mipLevel,
288
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to/from. */
289
    // destinationOrigin = [0, 0],
290
    /** Defines which aspects of the texture to copy to/from. */
291
    // destinationAspect = options.aspect,
292
  } = options;
×
293

294
  let {
295
    width = options.destinationTexture.width,
×
296
    height = options.destinationTexture.height
×
297
    // depthOrArrayLayers = 0
298
  } = options;
×
299

300
  const {framebuffer, destroyFramebuffer} = getFramebuffer(sourceTexture);
×
301
  const [sourceX = 0, sourceY = 0] = origin;
×
302
  const [destinationX, destinationY, destinationZ] = destinationOrigin;
×
303

304
  // @ts-expect-error native bindFramebuffer is overridden by our state tracker
305
  const prevHandle: WebGLFramebuffer | null = device.gl.bindFramebuffer(
×
306
    GL.FRAMEBUFFER,
307
    framebuffer.handle
308
  );
309
  // TODO - support gl.readBuffer (WebGL2 only)
310
  // const prevBuffer = gl.readBuffer(attachment);
311

312
  let texture: WEBGLTexture;
313
  let textureTarget: GL;
314
  if (destinationTexture instanceof WEBGLTexture) {
×
315
    texture = destinationTexture;
×
316
    width = Number.isFinite(width) ? width : texture.width;
×
317
    height = Number.isFinite(height) ? height : texture.height;
×
318
    texture._bind(0);
×
319
    textureTarget = texture.glTarget;
×
320
  } else {
321
    throw new Error('invalid destination');
×
322
  }
323

324
  switch (textureTarget) {
×
325
    case GL.TEXTURE_2D:
326
    case GL.TEXTURE_CUBE_MAP:
327
      device.gl.copyTexSubImage2D(
×
328
        textureTarget,
329
        destinationMipLevel,
330
        destinationX,
331
        destinationY,
332
        sourceX,
333
        sourceY,
334
        width,
335
        height
336
      );
337
      break;
×
338
    case GL.TEXTURE_2D_ARRAY:
339
    case GL.TEXTURE_3D:
340
      device.gl.copyTexSubImage3D(
×
341
        textureTarget,
342
        destinationMipLevel,
343
        destinationX,
344
        destinationY,
345
        destinationZ,
346
        sourceX,
347
        sourceY,
348
        width,
349
        height
350
      );
351
      break;
×
352
    default:
353
  }
354

355
  if (texture) {
×
356
    texture._unbind();
×
357
  }
358
  device.gl.bindFramebuffer(GL.FRAMEBUFFER, prevHandle);
×
359
  if (destroyFramebuffer) {
×
360
    framebuffer.destroy();
×
361
  }
362
}
363

364
/** Clear one mip level of a texture *
365
function _clearTexture(device: WebGLDevice, options: ClearTextureOptions) {
366
  const BORDER = 0;
367
  const {dimension, width, height, depth = 0, mipLevel = 0} = options;
368
  const {glInternalFormat, glFormat, glType, compressed} = options;
369
  const glTarget = getWebGLCubeFaceTarget(options.glTarget, dimension, depth);
370

371
  switch (dimension) {
372
    case '2d-array':
373
    case '3d':
374
      if (compressed) {
375
        // biome-ignore format: preserve layout
376
        device.gl.compressedTexImage3D(glTarget, mipLevel, glInternalFormat, width, height, depth, BORDER, null);
377
      } else {
378
        // biome-ignore format: preserve layout
379
        device.gl.texImage3D( glTarget, mipLevel, glInternalFormat, width, height, depth, BORDER, glFormat, glType, null);
380
      }
381
      break;
382

383
    case '2d':
384
    case 'cube':
385
      if (compressed) {
386
        // biome-ignore format: preserve layout
387
        device.gl.compressedTexImage2D(glTarget, mipLevel, glInternalFormat, width, height, BORDER, null);
388
      } else {
389
        // biome-ignore format: preserve layout
390
        device.gl.texImage2D(glTarget, mipLevel, glInternalFormat, width, height, BORDER, glFormat, glType, null);
391
      }
392
      break;
393

394
    default:
395
      throw new Error(dimension);
396
  }
397
}
398
  */
399

400
// function _readTexture(device: WebGLDevice, options: CopyTextureToBufferOptions) {}
401

402
// HELPERS
403

404
/**
405
 * In WebGL, cube maps specify faces by overriding target instead of using the depth parameter.
406
 * @note We still bind the texture using GL.TEXTURE_CUBE_MAP, but we need to use the face-specific target when setting mip levels.
407
 * @returns glTarget unchanged, if dimension !== 'cube'.
408
 */
409
export function getWebGLCubeFaceTarget(
410
  glTarget: GLTextureTarget,
411
  dimension: '1d' | '2d' | '2d-array' | 'cube' | 'cube-array' | '3d',
412
  level: number
413
): GLTextureTarget | GLTextureCubeMapTarget {
414
  return dimension === 'cube' ? GL.TEXTURE_CUBE_MAP_POSITIVE_X + level : glTarget;
×
415
}
416

417
/** Wrap a texture in a framebuffer so that we can use WebGL APIs that work on framebuffers */
418
function getFramebuffer(source: Texture | Framebuffer): {
419
  framebuffer: WEBGLFramebuffer;
420
  destroyFramebuffer: boolean;
421
} {
422
  if (source instanceof Texture) {
×
423
    const {width, height, id} = source;
×
424
    const framebuffer = source.device.createFramebuffer({
×
425
      id: `framebuffer-for-${id}`,
426
      width,
427
      height,
428
      colorAttachments: [source]
429
    }) as unknown as WEBGLFramebuffer;
430

431
    return {framebuffer, destroyFramebuffer: true};
×
432
  }
433
  return {framebuffer: source as unknown as WEBGLFramebuffer, destroyFramebuffer: false};
×
434
}
435

436
/**
437
 * Returns number of components in a specific readPixels WebGL format
438
 * @todo use shadertypes utils instead?
439
 */
440
export function glFormatToComponents(format: GL): 1 | 2 | 3 | 4 {
441
  switch (format) {
×
442
    case GL.ALPHA:
443
    case GL.R32F:
444
    case GL.RED:
445
      return 1;
×
446
    case GL.RG32F:
447
    case GL.RG:
448
      return 2;
×
449
    case GL.RGB:
450
    case GL.RGB32F:
451
      return 3;
×
452
    case GL.RGBA:
453
    case GL.RGBA32F:
454
      return 4;
×
455
    // TODO: Add support for additional WebGL2 formats
456
    default:
457
      throw new Error('GLFormat');
×
458
  }
459
}
460

461
/**
462
 * Return byte count for given readPixels WebGL type
463
 * @todo use shadertypes utils instead?
464
 */
465
export function glTypeToBytes(type: GL): 1 | 2 | 4 {
466
  switch (type) {
×
467
    case GL.UNSIGNED_BYTE:
468
      return 1;
×
469
    case GL.UNSIGNED_SHORT_5_6_5:
470
    case GL.UNSIGNED_SHORT_4_4_4_4:
471
    case GL.UNSIGNED_SHORT_5_5_5_1:
472
      return 2;
×
473
    case GL.FLOAT:
474
      return 4;
×
475
    // TODO: Add support for additional WebGL2 types
476
    default:
477
      throw new Error('GLType');
×
478
  }
479
}
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