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

visgl / luma.gl / 23412316304

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

push

github

web-flow
feat(engine): add async texture buffer read (#2439)

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

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

5
// prettier-ignore
6
// / <reference types="@webgpu/types" />
7

8
import type {
9
  Bindings,
10
  ComputePipeline,
11
  ComputeShaderLayout,
12
  DeviceInfo,
13
  DeviceLimits,
14
  DeviceFeature,
15
  DeviceTextureFormatCapabilities,
16
  VertexFormat,
17
  CanvasContextProps,
18
  PresentationContextProps,
19
  PresentationContext,
20
  BufferProps,
21
  SamplerProps,
22
  ShaderProps,
23
  TextureProps,
24
  Texture,
25
  ExternalTextureProps,
26
  FramebufferProps,
27
  RenderPipelineProps,
28
  ComputePipelineProps,
29
  VertexArrayProps,
30
  TransformFeedback,
31
  TransformFeedbackProps,
32
  QuerySet,
33
  QuerySetProps,
34
  DeviceProps,
35
  CommandEncoderProps,
36
  PipelineLayoutProps,
37
  RenderPipeline,
38
  ShaderLayout
39
} from '@luma.gl/core';
40
import {Device, DeviceFeatures} from '@luma.gl/core';
41
import {WebGPUBuffer} from './resources/webgpu-buffer';
42
import {WebGPUTexture} from './resources/webgpu-texture';
43
import {WebGPUExternalTexture} from './resources/webgpu-external-texture';
44
import {WebGPUSampler} from './resources/webgpu-sampler';
45
import {WebGPUShader} from './resources/webgpu-shader';
46
import {WebGPURenderPipeline} from './resources/webgpu-render-pipeline';
47
import {WebGPUFramebuffer} from './resources/webgpu-framebuffer';
48
import {WebGPUComputePipeline} from './resources/webgpu-compute-pipeline';
49
import {WebGPUVertexArray} from './resources/webgpu-vertex-array';
50

51
import {WebGPUCanvasContext} from './webgpu-canvas-context';
52
import {WebGPUPresentationContext} from './webgpu-presentation-context';
53
import {WebGPUCommandEncoder} from './resources/webgpu-command-encoder';
54
import {WebGPUCommandBuffer} from './resources/webgpu-command-buffer';
55
import {WebGPUQuerySet} from './resources/webgpu-query-set';
56
import {WebGPUPipelineLayout} from './resources/webgpu-pipeline-layout';
57
import {WebGPUFence} from './resources/webgpu-fence';
58

59
import {getShaderLayoutFromWGSL} from '../wgsl/get-shader-layout-wgsl';
60
import {generateMipmapsWebGPU} from './helpers/generate-mipmaps-webgpu';
61
import {getBindGroup} from './helpers/get-bind-group';
62
import {
63
  getCpuHotspotProfiler as getWebGPUCpuHotspotProfiler,
64
  getCpuHotspotSubmitReason as getWebGPUCpuHotspotSubmitReason,
65
  getTimestamp
66
} from './helpers/cpu-hotspot-profiler';
67

68
/** WebGPU Device implementation */
69
export class WebGPUDevice extends Device {
70
  /** The underlying WebGPU device */
71
  readonly handle: GPUDevice;
72
  /* The underlying WebGPU adapter */
73
  readonly adapter: GPUAdapter;
74
  /* The underlying WebGPU adapter's info */
75
  readonly adapterInfo: GPUAdapterInfo;
76

77
  /** type of this device */
78
  readonly type = 'webgpu';
25✔
79

80
  readonly preferredColorFormat = navigator.gpu.getPreferredCanvasFormat() as
25✔
81
    | 'rgba8unorm'
82
    | 'bgra8unorm';
83
  readonly preferredDepthFormat = 'depth24plus';
25✔
84

85
  readonly features: DeviceFeatures;
86
  readonly info: DeviceInfo;
87
  readonly limits: DeviceLimits;
88

89
  readonly lost: Promise<{reason: 'destroyed'; message: string}>;
90

91
  override canvasContext: WebGPUCanvasContext | null = null;
25✔
92

93
  private _isLost: boolean = false;
25✔
94
  private _defaultSampler: WebGPUSampler | null = null;
25✔
95
  commandEncoder: WebGPUCommandEncoder;
96

97
  override get [Symbol.toStringTag](): string {
98
    return 'WebGPUDevice';
×
99
  }
100

101
  override toString(): string {
UNCOV
102
    return `WebGPUDevice(${this.id})`;
×
103
  }
104

105
  constructor(
106
    props: DeviceProps,
107
    device: GPUDevice,
108
    adapter: GPUAdapter,
109
    adapterInfo: GPUAdapterInfo
110
  ) {
111
    super({...props, id: props.id || 'webgpu-device'});
25!
112
    this.handle = device;
25✔
113
    this.adapter = adapter;
25✔
114
    this.adapterInfo = adapterInfo;
25✔
115

116
    this.info = this._getInfo();
25✔
117
    this.features = this._getFeatures();
25✔
118
    this.limits = this.handle.limits;
25✔
119

120
    // Listen for uncaptured WebGPU errors
121
    device.addEventListener('uncapturederror', (event: Event) => {
25✔
UNCOV
122
      event.preventDefault();
×
123
      // TODO is this the right way to make sure the error is an Error instance?
124
      const errorMessage =
UNCOV
125
        event instanceof GPUUncapturedErrorEvent ? event.error.message : 'Unknown WebGPU error';
×
UNCOV
126
      this.reportError(new Error(errorMessage), this)();
×
UNCOV
127
      this.debug();
×
128
    });
129

130
    // "Context" loss handling
131
    this.lost = new Promise<{reason: 'destroyed'; message: string}>(async resolve => {
25✔
132
      const lostInfo = await this.handle.lost;
25✔
133
      this._isLost = true;
6✔
134
      resolve({reason: 'destroyed', message: lostInfo.message});
6✔
135
    });
136

137
    // Note: WebGPU devices can be created without a canvas, for compute shader purposes
138
    const canvasContextProps = Device._getCanvasContextProps(props);
25✔
139
    if (canvasContextProps) {
25!
140
      this.canvasContext = new WebGPUCanvasContext(this, this.adapter, canvasContextProps);
25✔
141
    }
142

143
    this.commandEncoder = this.createCommandEncoder({});
25✔
144
  }
145

146
  // TODO
147
  // Load the glslang module now so that it is available synchronously when compiling shaders
148
  // const {glsl = true} = props;
149
  // this.glslang = glsl && await loadGlslangModule();
150

151
  destroy(): void {
UNCOV
152
    this.commandEncoder?.destroy();
×
UNCOV
153
    this._defaultSampler?.destroy();
×
UNCOV
154
    this._defaultSampler = null;
×
UNCOV
155
    this.handle.destroy();
×
156
  }
157

158
  get isLost(): boolean {
159
    return this._isLost;
108✔
160
  }
161

162
  getShaderLayout(source: string) {
163
    return getShaderLayoutFromWGSL(source);
10✔
164
  }
165

166
  override isVertexFormatSupported(format: VertexFormat): boolean {
167
    const info = this.getVertexFormatInfo(format);
×
168
    return !info.webglOnly;
×
169
  }
170

171
  createBuffer(props: BufferProps | ArrayBuffer | ArrayBufferView): WebGPUBuffer {
172
    const newProps = this._normalizeBufferProps(props);
136✔
173
    return new WebGPUBuffer(this, newProps);
136✔
174
  }
175

176
  createTexture(props: TextureProps): WebGPUTexture {
177
    return new WebGPUTexture(this, props);
125✔
178
  }
179

180
  createExternalTexture(props: ExternalTextureProps): WebGPUExternalTexture {
181
    return new WebGPUExternalTexture(this, props);
×
182
  }
183

184
  createShader(props: ShaderProps): WebGPUShader {
185
    return new WebGPUShader(this, props);
48✔
186
  }
187

188
  createSampler(props: SamplerProps): WebGPUSampler {
189
    return new WebGPUSampler(this, props);
7✔
190
  }
191

192
  getDefaultSampler(): WebGPUSampler {
193
    this._defaultSampler ||= new WebGPUSampler(this, {
96✔
194
      id: `${this.id}-default-sampler`
195
    });
196
    return this._defaultSampler;
96✔
197
  }
198

199
  createRenderPipeline(props: RenderPipelineProps): WebGPURenderPipeline {
200
    return new WebGPURenderPipeline(this, props);
16✔
201
  }
202

203
  createFramebuffer(props: FramebufferProps): WebGPUFramebuffer {
204
    return new WebGPUFramebuffer(this, props);
36✔
205
  }
206

207
  createComputePipeline(props: ComputePipelineProps): WebGPUComputePipeline {
208
    return new WebGPUComputePipeline(this, props);
7✔
209
  }
210

211
  createVertexArray(props: VertexArrayProps): WebGPUVertexArray {
212
    return new WebGPUVertexArray(this, props);
5✔
213
  }
214

215
  override createCommandEncoder(props?: CommandEncoderProps): WebGPUCommandEncoder {
216
    return new WebGPUCommandEncoder(this, props);
112✔
217
  }
218

219
  // WebGPU specifics
220

221
  createTransformFeedback(props: TransformFeedbackProps): TransformFeedback {
222
    throw new Error('Transform feedback not supported in WebGPU');
×
223
  }
224

225
  override createQuerySet(props: QuerySetProps): QuerySet {
226
    return new WebGPUQuerySet(this, props);
6✔
227
  }
228

229
  override createFence(): WebGPUFence {
230
    return new WebGPUFence(this);
1✔
231
  }
232

233
  createCanvasContext(props: CanvasContextProps): WebGPUCanvasContext {
234
    return new WebGPUCanvasContext(this, this.adapter, props);
×
235
  }
236

237
  createPresentationContext(props?: PresentationContextProps): PresentationContext {
238
    return new WebGPUPresentationContext(this, props);
2✔
239
  }
240

241
  createPipelineLayout(props: PipelineLayoutProps): WebGPUPipelineLayout {
242
    return new WebGPUPipelineLayout(this, props);
16✔
243
  }
244

245
  override generateMipmapsWebGPU(texture: Texture): void {
246
    generateMipmapsWebGPU(this, texture);
12✔
247
  }
248

249
  override _createBindGroupLayoutWebGPU(
250
    pipeline: RenderPipeline | ComputePipeline,
251
    group: number
252
  ): GPUBindGroupLayout {
253
    return (pipeline as WebGPURenderPipeline | WebGPUComputePipeline).handle.getBindGroupLayout(
19✔
254
      group
255
    );
256
  }
257

258
  override _createBindGroupWebGPU(
259
    bindGroupLayout: unknown,
260
    shaderLayout: ShaderLayout | ComputeShaderLayout,
261
    bindings: Bindings,
262
    group: number
263
  ): GPUBindGroup | null {
264
    if (Object.keys(bindings).length === 0) {
45✔
265
      return this.handle.createBindGroup({
7✔
266
        layout: bindGroupLayout as GPUBindGroupLayout,
267
        entries: []
268
      });
269
    }
270

271
    return getBindGroup(this, bindGroupLayout as GPUBindGroupLayout, shaderLayout, bindings, group);
38✔
272
  }
273

274
  submit(commandBuffer?: WebGPUCommandBuffer): void {
275
    let submittedCommandEncoder: WebGPUCommandEncoder | null = null;
83✔
276
    if (!commandBuffer) {
83✔
277
      ({submittedCommandEncoder, commandBuffer} = this._finalizeDefaultCommandEncoderForSubmit());
80✔
278
    }
279

280
    const profiler = getWebGPUCpuHotspotProfiler(this);
83✔
281
    const startTime = profiler ? getTimestamp() : 0;
83✔
282
    const submitReason = getWebGPUCpuHotspotSubmitReason(this);
83✔
283
    try {
83✔
284
      this.pushErrorScope('validation');
83✔
285
      const queueSubmitStartTime = profiler ? getTimestamp() : 0;
83✔
286
      this.handle.queue.submit([commandBuffer.handle]);
83✔
287
      if (profiler) {
83✔
288
        profiler.queueSubmitCount = (profiler.queueSubmitCount || 0) + 1;
42✔
289
        profiler.queueSubmitTimeMs =
42✔
290
          (profiler.queueSubmitTimeMs || 0) + (getTimestamp() - queueSubmitStartTime);
50✔
291
      }
292
      this.popErrorScope((error: GPUError) => {
83✔
UNCOV
293
        this.reportError(new Error(`${this} command submission: ${error.message}`), this)();
×
UNCOV
294
        this.debug();
×
295
      });
296

297
      if (submittedCommandEncoder) {
83✔
298
        const submitResolveKickoffStartTime = profiler ? getTimestamp() : 0;
80✔
299
        scheduleMicrotask(() => {
80✔
300
          submittedCommandEncoder
80✔
301
            .resolveTimeProfilingQuerySet()
302
            .then(() => {
303
              this.commandEncoder._gpuTimeMs = submittedCommandEncoder._gpuTimeMs;
80✔
304
            })
305
            .catch(() => {});
306
        });
307
        if (profiler) {
80✔
308
          profiler.submitResolveKickoffCount = (profiler.submitResolveKickoffCount || 0) + 1;
42✔
309
          profiler.submitResolveKickoffTimeMs =
42✔
310
            (profiler.submitResolveKickoffTimeMs || 0) +
84✔
311
            (getTimestamp() - submitResolveKickoffStartTime);
312
        }
313
      }
314
    } finally {
315
      if (profiler) {
83✔
316
        profiler.submitCount = (profiler.submitCount || 0) + 1;
42✔
317
        profiler.submitTimeMs = (profiler.submitTimeMs || 0) + (getTimestamp() - startTime);
42✔
318
        const reasonCountKey =
319
          submitReason === 'query-readback' ? 'queryReadbackSubmitCount' : 'defaultSubmitCount';
42!
320
        const reasonTimeKey =
321
          submitReason === 'query-readback' ? 'queryReadbackSubmitTimeMs' : 'defaultSubmitTimeMs';
42!
322
        profiler[reasonCountKey] = (profiler[reasonCountKey] || 0) + 1;
42✔
323
        profiler[reasonTimeKey] = (profiler[reasonTimeKey] || 0) + (getTimestamp() - startTime);
42✔
324
      }
325
      const commandBufferDestroyStartTime = profiler ? getTimestamp() : 0;
83✔
326
      commandBuffer.destroy();
83✔
327
      if (profiler) {
83✔
328
        profiler.commandBufferDestroyCount = (profiler.commandBufferDestroyCount || 0) + 1;
42✔
329
        profiler.commandBufferDestroyTimeMs =
42✔
330
          (profiler.commandBufferDestroyTimeMs || 0) +
58✔
331
          (getTimestamp() - commandBufferDestroyStartTime);
332
      }
333
    }
334
  }
335

336
  private _finalizeDefaultCommandEncoderForSubmit(): {
337
    submittedCommandEncoder: WebGPUCommandEncoder;
338
    commandBuffer: WebGPUCommandBuffer;
339
  } {
340
    const submittedCommandEncoder = this.commandEncoder;
80✔
341
    if (
80!
342
      submittedCommandEncoder.getTimeProfilingSlotCount() > 0 &&
80!
343
      submittedCommandEncoder.getTimeProfilingQuerySet() instanceof WebGPUQuerySet
344
    ) {
NEW
345
      const querySet = submittedCommandEncoder.getTimeProfilingQuerySet() as WebGPUQuerySet;
×
NEW
346
      querySet._encodeResolveToReadBuffer(submittedCommandEncoder, {
×
347
        firstQuery: 0,
348
        queryCount: submittedCommandEncoder.getTimeProfilingSlotCount()
349
      });
350
    }
351

352
    const commandBuffer = submittedCommandEncoder.finish();
80✔
353
    this.commandEncoder.destroy();
80✔
354
    this.commandEncoder = this.createCommandEncoder({
80✔
355
      id: submittedCommandEncoder.props.id,
356
      timeProfilingQuerySet: submittedCommandEncoder.getTimeProfilingQuerySet()
357
    });
358

359
    return {submittedCommandEncoder, commandBuffer};
80✔
360
  }
361

362
  // WebGPU specific
363

364
  pushErrorScope(scope: 'validation' | 'out-of-memory'): void {
365
    if (!this.props.debug) {
1,537!
UNCOV
366
      return;
×
367
    }
368
    const profiler = getWebGPUCpuHotspotProfiler(this);
1,537✔
369
    const startTime = profiler ? getTimestamp() : 0;
1,537✔
370
    this.handle.pushErrorScope(scope);
1,537✔
371
    if (profiler) {
1,537✔
372
      profiler.errorScopePushCount = (profiler.errorScopePushCount || 0) + 1;
149✔
373
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
149✔
374
    }
375
  }
376

377
  popErrorScope(handler: (error: GPUError) => void): void {
378
    if (!this.props.debug) {
1,537!
UNCOV
379
      return;
×
380
    }
381
    const profiler = getWebGPUCpuHotspotProfiler(this);
1,537✔
382
    const startTime = profiler ? getTimestamp() : 0;
1,537✔
383
    this.handle
1,537✔
384
      .popErrorScope()
385
      .then((error: GPUError | null) => {
386
        if (error) {
1,156✔
387
          handler(error);
6✔
388
        }
389
      })
390
      .catch((error: unknown) => {
391
        if (this.shouldIgnoreDroppedInstanceError(error, 'popErrorScope')) {
381!
392
          return;
381✔
393
        }
394

395
        const errorMessage = error instanceof Error ? error.message : String(error);
×
396
        this.reportError(new Error(`${this} popErrorScope failed: ${errorMessage}`), this)();
381✔
397
        this.debug();
381✔
398
      });
399
    if (profiler) {
1,537✔
400
      profiler.errorScopePopCount = (profiler.errorScopePopCount || 0) + 1;
149✔
401
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
149✔
402
    }
403
  }
404

405
  // PRIVATE METHODS
406

407
  protected _getInfo(): DeviceInfo {
408
    const [driver, driverVersion] = ((this.adapterInfo as any).driver || '').split(' Version ');
25✔
409

410
    // See https://developer.chrome.com/blog/new-in-webgpu-120#adapter_information_updates
411
    const vendor = this.adapterInfo.vendor || this.adapter.__brand || 'unknown';
25!
412
    const renderer = driver || '';
25✔
413
    const version = driverVersion || '';
25✔
414
    const fallback = Boolean(
25✔
415
      (this.adapterInfo as any).isFallbackAdapter ??
25!
416
        (this.adapter as any).isFallbackAdapter ??
417
        false
418
    );
419
    const softwareRenderer = /SwiftShader/i.test(
25✔
420
      `${vendor} ${renderer} ${this.adapterInfo.architecture || ''}`
25!
421
    );
422

423
    const gpu =
424
      vendor === 'apple' ? 'apple' : softwareRenderer || fallback ? 'software' : 'unknown'; // 'nvidia' | 'amd' | 'intel' | 'apple' | 'unknown',
25!
425
    const gpuArchitecture = this.adapterInfo.architecture || 'unknown';
25!
426
    const gpuBackend = (this.adapterInfo as any).backend || 'unknown';
25✔
427
    const gpuType =
25✔
428
      ((this.adapterInfo as any).type || '').split(' ')[0].toLowerCase() ||
50✔
429
      (softwareRenderer || fallback ? 'cpu' : 'unknown');
50!
430

431
    return {
25✔
432
      type: 'webgpu',
433
      vendor,
434
      renderer,
435
      version,
436
      gpu,
437
      gpuType,
438
      gpuBackend,
439
      gpuArchitecture,
440
      fallback,
441
      shadingLanguage: 'wgsl',
442
      shadingLanguageVersion: 100
443
    };
444
  }
445

446
  shouldIgnoreDroppedInstanceError(error: unknown, operation?: string): boolean {
447
    const errorMessage = error instanceof Error ? error.message : String(error);
384!
448
    return (
384✔
449
      errorMessage.includes('Instance dropped') &&
1,536!
450
      (!operation || errorMessage.includes(operation)) &&
451
      (this._isLost ||
452
        this.info.gpu === 'software' ||
453
        this.info.gpuType === 'cpu' ||
454
        Boolean(this.info.fallback))
455
    );
456
  }
457

458
  protected _getFeatures(): DeviceFeatures {
459
    // Initialize with actual WebGPU Features (note that unknown features may not be in DeviceFeature type)
460
    const features = new Set<DeviceFeature>(this.handle.features as Set<DeviceFeature>);
25✔
461
    // Fixups for pre-standard names: https://github.com/webgpu-native/webgpu-headers/issues/133
462
    // @ts-expect-error Chrome Canary v99
463
    if (features.has('depth-clamping')) {
25!
464
      // @ts-expect-error Chrome Canary v99
465
      features.delete('depth-clamping');
×
466
      features.add('depth-clip-control');
×
467
    }
468

469
    // Some subsets of WebGPU extensions correspond to WebGL extensions
470
    if (features.has('texture-compression-bc')) {
25!
471
      features.add('texture-compression-bc5-webgl');
25✔
472
    }
473

474
    if (this.handle.features.has('chromium-experimental-norm16-texture-formats')) {
25!
475
      features.add('norm16-renderable-webgl');
×
476
    }
477

478
    if (this.handle.features.has('chromium-experimental-snorm16-texture-formats')) {
25!
479
      features.add('snorm16-renderable-webgl');
×
480
    }
481

482
    const WEBGPU_ALWAYS_FEATURES: DeviceFeature[] = [
25✔
483
      'compilation-status-async-webgl',
484
      'float32-renderable-webgl',
485
      'float16-renderable-webgl',
486
      'norm16-renderable-webgl',
487
      'texture-filterable-anisotropic-webgl',
488
      'shader-noperspective-interpolation-webgl'
489
    ];
490

491
    for (const feature of WEBGPU_ALWAYS_FEATURES) {
25✔
492
      features.add(feature);
150✔
493
    }
494

495
    return new DeviceFeatures(Array.from(features), this.props._disabledFeatures);
25✔
496
  }
497

498
  override _getDeviceSpecificTextureFormatCapabilities(
499
    capabilities: DeviceTextureFormatCapabilities
500
  ): DeviceTextureFormatCapabilities {
501
    const {format} = capabilities;
72✔
502
    if (format.includes('webgl')) {
72✔
503
      return {format, create: false, render: false, filter: false, blend: false, store: false};
11✔
504
    }
505
    return capabilities;
61✔
506
  }
507
}
508

509
function scheduleMicrotask(callback: () => void): void {
510
  if (globalThis.queueMicrotask) {
80!
511
    globalThis.queueMicrotask(callback);
80✔
512
    return;
80✔
513
  }
514
  Promise.resolve()
×
515
    .then(callback)
516
    .catch(() => {});
517
}
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