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

visgl / luma.gl / 23357312823

20 Mar 2026 06:35PM UTC coverage: 58.158% (+5.9%) from 52.213%
23357312823

Pull #2555

github

web-flow
Merge 71b78bbe2 into fc5791b65
Pull Request #2555: chore: Run tests on src instead of dist

3021 of 6029 branches covered (50.11%)

Branch coverage included in aggregate %.

7102 of 11377 relevant lines covered (62.42%)

243.33 hits per line

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

66.46
/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;
179✔
67
  commands: Command[] = [];
179✔
68

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

74
  _executeCommands(commands: Command[] = this.commands) {
67✔
75
    for (const command of commands) {
67✔
76
      switch (command.name) {
18!
77
        case 'copy-buffer-to-buffer':
78
          _copyBufferToBuffer(this.device, command.options);
2✔
79
          break;
2✔
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);
15✔
85
          break;
15✔
86
        case 'copy-texture-to-texture':
87
          _copyTextureToTexture(this.device, command.options);
1✔
88
          break;
1✔
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;
2✔
101
  const destination = options.destinationBuffer as WEBGLBuffer;
2✔
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);
2✔
106
  device.gl.bindBuffer(GL.COPY_WRITE_BUFFER, destination.handle);
2✔
107
  device.gl.copyBufferSubData(
2✔
108
    GL.COPY_READ_BUFFER,
109
    GL.COPY_WRITE_BUFFER,
110
    options.sourceOffset ?? 0,
3✔
111
    options.destinationOffset ?? 0,
3✔
112
    options.size
113
  );
114
  device.gl.bindBuffer(GL.COPY_READ_BUFFER, null);
2✔
115
  device.gl.bindBuffer(GL.COPY_WRITE_BUFFER, null);
2✔
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,
15✔
136
    /** Defines which aspects of the texture to copy to/from. */
137
    aspect = 'all',
15✔
138

139
    /** Width to copy */
140
    width = options.sourceTexture.width,
15✔
141
    /** Height to copy */
142
    height = options.sourceTexture.height,
15✔
143
    depthOrArrayLayers = 0,
15✔
144
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to/from. */
145
    origin = [0, 0, 0],
15✔
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,
15✔
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;
15✔
163

164
  // TODO - Not possible to read just stencil or depth part in WebGL?
165
  if (aspect !== 'all') {
15!
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) {
15!
171
    throw new Error('not implemented');
×
172
  }
173

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

183
    const sourceParams = getTextureFormatWebGL(colorAttachment0.texture.props.format);
15✔
184
    const sourceFormat = sourceParams.format;
15✔
185
    const sourceType = sourceParams.type;
15✔
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);
15✔
196
    // @ts-expect-error native bindFramebuffer is overridden by our state tracker
197
    prevHandle = device.gl.bindFramebuffer(GL.FRAMEBUFFER, framebuffer.handle);
15✔
198

199
    device.gl.readPixels(
15✔
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);
15✔
210
    // prevHandle may be unassigned if the try block failed before binding
211
    if (prevHandle !== undefined) {
15!
212
      device.gl.bindFramebuffer(GL.FRAMEBUFFER, prevHandle);
15✔
213
    }
214

215
    if (destroyFramebuffer) {
15✔
216
      framebuffer.destroy();
9✔
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,
1✔
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],
1✔
254

255
    /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to. */
256
    destinationOrigin = [0, 0, 0],
1✔
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,
266
  } = options;
1✔
267

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

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

278
  // @ts-expect-error native bindFramebuffer is overridden by our state tracker
279
  const prevHandle: WebGLFramebuffer | null = device.gl.bindFramebuffer(
1✔
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;
288
  if (destinationTexture instanceof WEBGLTexture) {
1!
289
    texture = destinationTexture;
1✔
290
    width = Number.isFinite(width) ? width : texture.width;
1!
291
    height = Number.isFinite(height) ? height : texture.height;
1!
292
    texture._bind(0);
1✔
293
    textureTarget = texture.glTarget;
1✔
294
  } else {
295
    throw new Error('invalid destination');
×
296
  }
297

298
  switch (textureTarget) {
1!
299
    case GL.TEXTURE_2D:
300
    case GL.TEXTURE_CUBE_MAP:
301
      device.gl.copyTexSubImage2D(
1✔
302
        textureTarget,
303
        destinationMipLevel,
304
        destinationX,
305
        destinationY,
306
        sourceX,
307
        sourceY,
308
        width,
309
        height
310
      );
311
      break;
1✔
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

329
  if (texture) {
1!
330
    texture._unbind();
1✔
331
  }
332
  device.gl.bindFramebuffer(GL.FRAMEBUFFER, prevHandle);
1✔
333
  if (destroyFramebuffer) {
1!
334
    framebuffer.destroy();
1✔
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) {
16✔
397
    const {width, height, id} = source;
10✔
398
    const framebuffer = source.device.createFramebuffer({
10✔
399
      id: `framebuffer-for-${id}`,
400
      width,
401
      height,
402
      colorAttachments: [source]
403
    }) as unknown as WEBGLFramebuffer;
404

405
    return {framebuffer, destroyFramebuffer: true};
10✔
406
  }
407
  return {framebuffer: source as unknown as WEBGLFramebuffer, destroyFramebuffer: false};
6✔
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