• 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

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

46
import {WebGPUCanvasContext} from './webgpu-canvas-context';
47
import {WebGPUPresentationContext} from './webgpu-presentation-context';
48
import {WebGPUCommandEncoder} from './resources/webgpu-command-encoder';
49
import {WebGPUCommandBuffer} from './resources/webgpu-command-buffer';
50
import {WebGPUQuerySet} from './resources/webgpu-query-set';
51
import {WebGPUPipelineLayout} from './resources/webgpu-pipeline-layout';
52
import {WebGPUFence} from './resources/webgpu-fence';
53

54
import {getShaderLayoutFromWGSL} from '../wgsl/get-shader-layout-wgsl';
55
import {generateMipmapsWebGPU} from './helpers/generate-mipmaps-webgpu';
56
import {
57
  getCpuHotspotProfiler as getWebGPUCpuHotspotProfiler,
58
  getCpuHotspotSubmitReason as getWebGPUCpuHotspotSubmitReason,
59
  getTimestamp
60
} from './helpers/cpu-hotspot-profiler';
61

62
/** WebGPU Device implementation */
63
export class WebGPUDevice extends Device {
64
  /** The underlying WebGPU device */
65
  readonly handle: GPUDevice;
66
  /* The underlying WebGPU adapter */
67
  readonly adapter: GPUAdapter;
68
  /* The underlying WebGPU adapter's info */
69
  readonly adapterInfo: GPUAdapterInfo;
70

71
  /** type of this device */
72
  readonly type = 'webgpu';
28✔
73

74
  readonly preferredColorFormat = navigator.gpu.getPreferredCanvasFormat() as
28✔
75
    | 'rgba8unorm'
76
    | 'bgra8unorm';
77
  readonly preferredDepthFormat = 'depth24plus';
28✔
78

79
  readonly features: DeviceFeatures;
80
  readonly info: DeviceInfo;
81
  readonly limits: DeviceLimits;
82

83
  readonly lost: Promise<{reason: 'destroyed'; message: string}>;
84

85
  override canvasContext: WebGPUCanvasContext | null = null;
28✔
86

87
  private _isLost: boolean = false;
28✔
88
  private _defaultSampler: WebGPUSampler | null = null;
28✔
89
  commandEncoder: WebGPUCommandEncoder;
90

91
  override get [Symbol.toStringTag](): string {
92
    return 'WebGPUDevice';
×
93
  }
94

95
  override toString(): string {
96
    return `WebGPUDevice(${this.id})`;
×
97
  }
98

99
  constructor(
100
    props: DeviceProps,
101
    device: GPUDevice,
102
    adapter: GPUAdapter,
103
    adapterInfo: GPUAdapterInfo
104
  ) {
105
    super({...props, id: props.id || 'webgpu-device'});
28!
106
    this.handle = device;
28✔
107
    this.adapter = adapter;
28✔
108
    this.adapterInfo = adapterInfo;
28✔
109

110
    this.info = this._getInfo();
28✔
111
    this.features = this._getFeatures();
28✔
112
    this.limits = this.handle.limits;
28✔
113

114
    // Listen for uncaptured WebGPU errors
115
    device.addEventListener('uncapturederror', (event: Event) => {
28✔
116
      event.preventDefault();
×
117
      // TODO is this the right way to make sure the error is an Error instance?
118
      const errorMessage =
119
        event instanceof GPUUncapturedErrorEvent ? event.error.message : 'Unknown WebGPU error';
×
120
      this.reportError(new Error(errorMessage), this)();
×
121
      this.debug();
×
122
    });
123

124
    // "Context" loss handling
125
    this.lost = new Promise<{reason: 'destroyed'; message: string}>(async resolve => {
28✔
126
      const lostInfo = await this.handle.lost;
28✔
127
      this._isLost = true;
8✔
128
      resolve({reason: 'destroyed', message: lostInfo.message});
8✔
129
    });
130

131
    // Note: WebGPU devices can be created without a canvas, for compute shader purposes
132
    const canvasContextProps = Device._getCanvasContextProps(props);
28✔
133
    if (canvasContextProps) {
28!
134
      this.canvasContext = new WebGPUCanvasContext(this, this.adapter, canvasContextProps);
28✔
135
    }
136

137
    this.commandEncoder = this.createCommandEncoder({});
28✔
138
  }
139

140
  // TODO
141
  // Load the glslang module now so that it is available synchronously when compiling shaders
142
  // const {glsl = true} = props;
143
  // this.glslang = glsl && await loadGlslangModule();
144

145
  destroy(): void {
146
    this.commandEncoder?.destroy();
2✔
147
    this._defaultSampler?.destroy();
2✔
148
    this._defaultSampler = null;
2✔
149
    this.handle.destroy();
2✔
150
  }
151

152
  get isLost(): boolean {
153
    return this._isLost;
2✔
154
  }
155

156
  getShaderLayout(source: string) {
157
    return getShaderLayoutFromWGSL(source);
9✔
158
  }
159

160
  override isVertexFormatSupported(format: VertexFormat): boolean {
161
    const info = this.getVertexFormatInfo(format);
×
162
    return !info.webglOnly;
×
163
  }
164

165
  createBuffer(props: BufferProps | ArrayBuffer | ArrayBufferView): WebGPUBuffer {
166
    const newProps = this._normalizeBufferProps(props);
135✔
167
    return new WebGPUBuffer(this, newProps);
135✔
168
  }
169

170
  createTexture(props: TextureProps): WebGPUTexture {
171
    return new WebGPUTexture(this, props);
127✔
172
  }
173

174
  createExternalTexture(props: ExternalTextureProps): WebGPUExternalTexture {
175
    return new WebGPUExternalTexture(this, props);
×
176
  }
177

178
  createShader(props: ShaderProps): WebGPUShader {
179
    return new WebGPUShader(this, props);
49✔
180
  }
181

182
  createSampler(props: SamplerProps): WebGPUSampler {
183
    return new WebGPUSampler(this, props);
7✔
184
  }
185

186
  getDefaultSampler(): WebGPUSampler {
187
    this._defaultSampler ||= new WebGPUSampler(this, {
98✔
188
      id: `${this.id}-default-sampler`
189
    });
190
    return this._defaultSampler;
98✔
191
  }
192

193
  createRenderPipeline(props: RenderPipelineProps): WebGPURenderPipeline {
194
    return new WebGPURenderPipeline(this, props);
15✔
195
  }
196

197
  createFramebuffer(props: FramebufferProps): WebGPUFramebuffer {
198
    return new WebGPUFramebuffer(this, props);
36✔
199
  }
200

201
  createComputePipeline(props: ComputePipelineProps): WebGPUComputePipeline {
202
    return new WebGPUComputePipeline(this, props);
9✔
203
  }
204

205
  createVertexArray(props: VertexArrayProps): WebGPUVertexArray {
206
    return new WebGPUVertexArray(this, props);
4✔
207
  }
208

209
  override createCommandEncoder(props?: CommandEncoderProps): WebGPUCommandEncoder {
210
    return new WebGPUCommandEncoder(this, props);
120✔
211
  }
212

213
  // WebGPU specifics
214

215
  createTransformFeedback(props: TransformFeedbackProps): TransformFeedback {
216
    throw new Error('Transform feedback not supported in WebGPU');
×
217
  }
218

219
  override createQuerySet(props: QuerySetProps): QuerySet {
220
    return new WebGPUQuerySet(this, props);
7✔
221
  }
222

223
  override createFence(): WebGPUFence {
224
    return new WebGPUFence(this);
1✔
225
  }
226

227
  createCanvasContext(props: CanvasContextProps): WebGPUCanvasContext {
228
    return new WebGPUCanvasContext(this, this.adapter, props);
×
229
  }
230

231
  createPresentationContext(props?: PresentationContextProps): PresentationContext {
232
    return new WebGPUPresentationContext(this, props);
2✔
233
  }
234

235
  createPipelineLayout(props: PipelineLayoutProps): WebGPUPipelineLayout {
236
    return new WebGPUPipelineLayout(this, props);
15✔
237
  }
238

239
  override generateMipmapsWebGPU(texture: Texture): void {
240
    generateMipmapsWebGPU(this, texture);
12✔
241
  }
242

243
  submit(commandBuffer?: WebGPUCommandBuffer): void {
244
    let submittedCommandEncoder: WebGPUCommandEncoder | null = null;
86✔
245
    if (!commandBuffer) {
86✔
246
      submittedCommandEncoder = this.commandEncoder;
83✔
247
      if (
83!
248
        submittedCommandEncoder.getTimeProfilingSlotCount() > 0 &&
83!
249
        submittedCommandEncoder.getTimeProfilingQuerySet() instanceof WebGPUQuerySet
250
      ) {
251
        const querySet = submittedCommandEncoder.getTimeProfilingQuerySet() as WebGPUQuerySet;
×
252
        querySet._encodeResolveToReadBuffer(submittedCommandEncoder, {
×
253
          firstQuery: 0,
254
          queryCount: submittedCommandEncoder.getTimeProfilingSlotCount()
255
        });
256
      }
257
      commandBuffer = submittedCommandEncoder.finish();
83✔
258
      this.commandEncoder.destroy();
83✔
259
      this.commandEncoder = this.createCommandEncoder({
83✔
260
        id: submittedCommandEncoder.props.id,
261
        timeProfilingQuerySet: submittedCommandEncoder.getTimeProfilingQuerySet()
262
      });
263
    }
264

265
    const profiler = getWebGPUCpuHotspotProfiler(this);
86✔
266
    const startTime = profiler ? getTimestamp() : 0;
86✔
267
    const submitReason = getWebGPUCpuHotspotSubmitReason(this);
86✔
268
    try {
86✔
269
      this.pushErrorScope('validation');
86✔
270
      const queueSubmitStartTime = profiler ? getTimestamp() : 0;
86✔
271
      this.handle.queue.submit([commandBuffer.handle]);
86✔
272
      if (profiler) {
86✔
273
        profiler.queueSubmitCount = (profiler.queueSubmitCount || 0) + 1;
44✔
274
        profiler.queueSubmitTimeMs =
44✔
275
          (profiler.queueSubmitTimeMs || 0) + (getTimestamp() - queueSubmitStartTime);
64✔
276
      }
277
      this.popErrorScope((error: GPUError) => {
86✔
278
        this.reportError(new Error(`${this} command submission: ${error.message}`), this)();
×
279
        this.debug();
×
280
      });
281

282
      if (submittedCommandEncoder) {
86✔
283
        const submitResolveKickoffStartTime = profiler ? getTimestamp() : 0;
83✔
284
        scheduleMicrotask(() => {
83✔
285
          submittedCommandEncoder
83✔
286
            .resolveTimeProfilingQuerySet()
287
            .then(() => {
288
              this.commandEncoder._gpuTimeMs = submittedCommandEncoder._gpuTimeMs;
83✔
289
            })
290
            .catch(() => {});
291
        });
292
        if (profiler) {
83✔
293
          profiler.submitResolveKickoffCount = (profiler.submitResolveKickoffCount || 0) + 1;
44✔
294
          profiler.submitResolveKickoffTimeMs =
44✔
295
            (profiler.submitResolveKickoffTimeMs || 0) +
82✔
296
            (getTimestamp() - submitResolveKickoffStartTime);
297
        }
298
      }
299
    } finally {
300
      if (profiler) {
86✔
301
        profiler.submitCount = (profiler.submitCount || 0) + 1;
44✔
302
        profiler.submitTimeMs = (profiler.submitTimeMs || 0) + (getTimestamp() - startTime);
44✔
303
        const reasonCountKey =
304
          submitReason === 'query-readback' ? 'queryReadbackSubmitCount' : 'defaultSubmitCount';
44!
305
        const reasonTimeKey =
306
          submitReason === 'query-readback' ? 'queryReadbackSubmitTimeMs' : 'defaultSubmitTimeMs';
44!
307
        profiler[reasonCountKey] = (profiler[reasonCountKey] || 0) + 1;
44✔
308
        profiler[reasonTimeKey] = (profiler[reasonTimeKey] || 0) + (getTimestamp() - startTime);
44✔
309
      }
310
      const commandBufferDestroyStartTime = profiler ? getTimestamp() : 0;
86✔
311
      commandBuffer.destroy();
86✔
312
      if (profiler) {
86✔
313
        profiler.commandBufferDestroyCount = (profiler.commandBufferDestroyCount || 0) + 1;
44✔
314
        profiler.commandBufferDestroyTimeMs =
44✔
315
          (profiler.commandBufferDestroyTimeMs || 0) +
50✔
316
          (getTimestamp() - commandBufferDestroyStartTime);
317
      }
318
    }
319
  }
320

321
  // WebGPU specific
322

323
  pushErrorScope(scope: 'validation' | 'out-of-memory'): void {
324
    if (!this.props.debug) {
1,560✔
325
      return;
21✔
326
    }
327
    const profiler = getWebGPUCpuHotspotProfiler(this);
1,539✔
328
    const startTime = profiler ? getTimestamp() : 0;
1,539✔
329
    this.handle.pushErrorScope(scope);
1,560✔
330
    if (profiler) {
1,560✔
331
      profiler.errorScopePushCount = (profiler.errorScopePushCount || 0) + 1;
172✔
332
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
172✔
333
    }
334
  }
335

336
  popErrorScope(handler: (error: GPUError) => void): void {
337
    if (!this.props.debug) {
1,560✔
338
      return;
21✔
339
    }
340
    const profiler = getWebGPUCpuHotspotProfiler(this);
1,539✔
341
    const startTime = profiler ? getTimestamp() : 0;
1,539✔
342
    this.handle
1,560✔
343
      .popErrorScope()
344
      .then((error: GPUError | null) => {
345
        if (error) {
1,158✔
346
          handler(error);
6✔
347
        }
348
      })
349
      .catch((error: unknown) => {
350
        if (this.shouldIgnoreDroppedInstanceError(error, 'popErrorScope')) {
381!
351
          return;
381✔
352
        }
353

NEW
354
        const errorMessage = error instanceof Error ? error.message : String(error);
×
355
        this.reportError(new Error(`${this} popErrorScope failed: ${errorMessage}`), this)();
381✔
356
        this.debug();
381✔
357
      });
358
    if (profiler) {
1,560✔
359
      profiler.errorScopePopCount = (profiler.errorScopePopCount || 0) + 1;
172✔
360
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
172✔
361
    }
362
  }
363

364
  // PRIVATE METHODS
365

366
  protected _getInfo(): DeviceInfo {
367
    const [driver, driverVersion] = ((this.adapterInfo as any).driver || '').split(' Version ');
28✔
368

369
    // See https://developer.chrome.com/blog/new-in-webgpu-120#adapter_information_updates
370
    const vendor = this.adapterInfo.vendor || this.adapter.__brand || 'unknown';
28!
371
    const renderer = driver || '';
28✔
372
    const version = driverVersion || '';
28✔
373
    const fallback = Boolean(
28✔
374
      (this.adapterInfo as any).isFallbackAdapter ??
28!
375
        (this.adapter as any).isFallbackAdapter ??
376
        false
377
    );
378
    const softwareRenderer = /SwiftShader/i.test(
28✔
379
      `${vendor} ${renderer} ${this.adapterInfo.architecture || ''}`
28!
380
    );
381

382
    const gpu =
383
      vendor === 'apple' ? 'apple' : softwareRenderer || fallback ? 'software' : 'unknown'; // 'nvidia' | 'amd' | 'intel' | 'apple' | 'unknown',
28!
384
    const gpuArchitecture = this.adapterInfo.architecture || 'unknown';
28!
385
    const gpuBackend = (this.adapterInfo as any).backend || 'unknown';
28✔
386
    const gpuType =
28✔
387
      ((this.adapterInfo as any).type || '').split(' ')[0].toLowerCase() ||
56✔
388
      (softwareRenderer || fallback ? 'cpu' : 'unknown');
56!
389

390
    return {
28✔
391
      type: 'webgpu',
392
      vendor,
393
      renderer,
394
      version,
395
      gpu,
396
      gpuType,
397
      gpuBackend,
398
      gpuArchitecture,
399
      fallback,
400
      shadingLanguage: 'wgsl',
401
      shadingLanguageVersion: 100
402
    };
403
  }
404

405
  shouldIgnoreDroppedInstanceError(error: unknown, operation?: string): boolean {
406
    const errorMessage = error instanceof Error ? error.message : String(error);
384!
407
    return (
384✔
408
      errorMessage.includes('Instance dropped') &&
1,536!
409
      (!operation || errorMessage.includes(operation)) &&
410
      (this._isLost ||
411
        this.info.gpu === 'software' ||
412
        this.info.gpuType === 'cpu' ||
413
        Boolean(this.info.fallback))
414
    );
415
  }
416

417
  protected _getFeatures(): DeviceFeatures {
418
    // Initialize with actual WebGPU Features (note that unknown features may not be in DeviceFeature type)
419
    const features = new Set<DeviceFeature>(this.handle.features as Set<DeviceFeature>);
28✔
420
    // Fixups for pre-standard names: https://github.com/webgpu-native/webgpu-headers/issues/133
421
    // @ts-expect-error Chrome Canary v99
422
    if (features.has('depth-clamping')) {
28!
423
      // @ts-expect-error Chrome Canary v99
424
      features.delete('depth-clamping');
×
425
      features.add('depth-clip-control');
×
426
    }
427

428
    // Some subsets of WebGPU extensions correspond to WebGL extensions
429
    if (features.has('texture-compression-bc')) {
28!
430
      features.add('texture-compression-bc5-webgl');
28✔
431
    }
432

433
    if (this.handle.features.has('chromium-experimental-norm16-texture-formats')) {
28!
434
      features.add('norm16-renderable-webgl');
×
435
    }
436

437
    if (this.handle.features.has('chromium-experimental-snorm16-texture-formats')) {
28!
438
      features.add('snorm16-renderable-webgl');
×
439
    }
440

441
    const WEBGPU_ALWAYS_FEATURES: DeviceFeature[] = [
28✔
442
      'compilation-status-async-webgl',
443
      'float32-renderable-webgl',
444
      'float16-renderable-webgl',
445
      'norm16-renderable-webgl',
446
      'texture-filterable-anisotropic-webgl',
447
      'shader-noperspective-interpolation-webgl'
448
    ];
449

450
    for (const feature of WEBGPU_ALWAYS_FEATURES) {
28✔
451
      features.add(feature);
168✔
452
    }
453

454
    return new DeviceFeatures(Array.from(features), this.props._disabledFeatures);
28✔
455
  }
456

457
  override _getDeviceSpecificTextureFormatCapabilities(
458
    capabilities: DeviceTextureFormatCapabilities
459
  ): DeviceTextureFormatCapabilities {
460
    const {format} = capabilities;
72✔
461
    if (format.includes('webgl')) {
72✔
462
      return {format, create: false, render: false, filter: false, blend: false, store: false};
11✔
463
    }
464
    return capabilities;
61✔
465
  }
466
}
467

468
function scheduleMicrotask(callback: () => void): void {
469
  if (globalThis.queueMicrotask) {
83!
470
    globalThis.queueMicrotask(callback);
83✔
471
    return;
83✔
472
  }
473
  Promise.resolve()
×
474
    .then(callback)
475
    .catch(() => {});
476
}
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