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

visgl / luma.gl / 23412192171

22 Mar 2026 08:49PM UTC coverage: 73.59% (-0.6%) from 74.227%
23412192171

Pull #2439

github

web-flow
Merge 99091cdc8 into 7c172e633
Pull Request #2439: feat(engine): add async texture buffer read

4597 of 7074 branches covered (64.98%)

Branch coverage included in aggregate %.

111 of 213 new or added lines in 20 files covered. (52.11%)

40 existing lines in 8 files now uncovered.

10525 of 13475 relevant lines covered (78.11%)

263.46 hits per line

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

19.88
/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 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;
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':
78
          _copyBufferToBuffer(this.device, command.options);
×
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':
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;
×
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
105
  device.gl.bindBuffer(GL.COPY_READ_BUFFER, source.handle);
×
106
  device.gl.bindBuffer(GL.COPY_WRITE_BUFFER, destination.handle);
×
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
  );
114
  device.gl.bindBuffer(GL.COPY_READ_BUFFER, null);
×
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 {
NEW
123
  throw new Error('copyBufferToTexture is not supported in WebGL');
×
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
    sourceTexture,
133
    mipLevel = 0,
3✔
134
    aspect = 'all',
3✔
135
    width = options.sourceTexture.width,
3✔
136
    height = options.sourceTexture.height,
3✔
137
    depthOrArrayLayers,
138
    origin = [0, 0, 0],
3✔
139
    destinationBuffer,
140
    byteOffset = 0,
3✔
141
    bytesPerRow,
142
    rowsPerImage
143
  } = options;
3✔
144

145
  if (sourceTexture instanceof Texture) {
3!
146
    sourceTexture.readBuffer(
3✔
147
      {
148
        x: origin[0] ?? 0,
3!
149
        y: origin[1] ?? 0,
3!
150
        z: origin[2] ?? 0,
3!
151
        width,
152
        height,
153
        depthOrArrayLayers,
154
        mipLevel,
155
        aspect,
156
        byteOffset
157
      } as TextureReadOptions & {byteOffset?: number},
158
      destinationBuffer
159
    );
160
    return;
3✔
161
  }
162

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

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

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

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

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

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

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

UNCOV
214
    if (destroyFramebuffer) {
×
UNCOV
215
      framebuffer.destroy();
×
216
    }
217
  }
218
}
219

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

375
// HELPERS
376

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

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

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

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

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