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

visgl / luma.gl / 29583177873

17 Jul 2026 01:14PM UTC coverage: 72.4%. First build
29583177873

Pull #2742

github

web-flow
Merge f290f1071 into 01dd287e7
Pull Request #2742: fix(core): manage device canvas context lifecycle

11341 of 17632 branches covered (64.32%)

Branch coverage included in aggregate %.

73 of 81 new or added lines in 11 files covered. (90.12%)

21961 of 28365 relevant lines covered (77.42%)

5588.52 hits per line

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

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

5
// biome-ignore format: preserve layout
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
  CommandEncoder,
37
  PipelineLayoutProps,
38
  RenderPipeline,
39
  RenderBundleEncoderProps,
40
  ShaderLayout
41
} from '@luma.gl/core';
42
import {Buffer, Device, DeviceFeatures, isHTMLInCanvasSupported} from '@luma.gl/core';
43
import {WebGPUBuffer} from './resources/webgpu-buffer';
44
import {WebGPUTexture} from './resources/webgpu-texture';
45
import {WebGPUExternalTexture} from './resources/webgpu-external-texture';
46
import {WebGPUSampler, type WebGPUSamplerProps} from './resources/webgpu-sampler';
47
import {WebGPUShader} from './resources/webgpu-shader';
48
import {WebGPURenderPipeline} from './resources/webgpu-render-pipeline';
49
import {WebGPURenderBundleEncoder} from './resources/webgpu-render-bundle';
50
import {WebGPUFramebuffer} from './resources/webgpu-framebuffer';
51
import {WebGPUComputePipeline} from './resources/webgpu-compute-pipeline';
52
import {WebGPUVertexArray} from './resources/webgpu-vertex-array';
53

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

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

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

80
  /** type of this device */
81
  readonly type = 'webgpu';
72✔
82

83
  readonly preferredColorFormat = navigator.gpu.getPreferredCanvasFormat() as
72✔
84
    | 'rgba8unorm'
85
    | 'bgra8unorm';
86
  readonly preferredDepthFormat = 'depth24plus';
72✔
87

88
  readonly features: DeviceFeatures;
89
  readonly info: DeviceInfo;
90
  readonly limits: DeviceLimits;
91

92
  readonly lost: Promise<{reason: 'destroyed'; message: string}>;
93

94
  override canvasContext: WebGPUCanvasContext | null = null;
72✔
95

96
  private _isLost: boolean = false;
72✔
97
  private _defaultSampler: WebGPUSampler | null = null;
72✔
98
  commandEncoder: WebGPUCommandEncoder;
99

100
  override get [Symbol.toStringTag](): string {
101
    return 'WebGPUDevice';
×
102
  }
103

104
  override toString(): string {
105
    return `WebGPUDevice(${this.id})`;
2✔
106
  }
107

108
  constructor(
109
    props: DeviceProps,
110
    device: GPUDevice,
111
    adapter: GPUAdapter,
112
    adapterInfo: GPUAdapterInfo
113
  ) {
114
    super({...props, id: props.id || 'webgpu-device'});
72✔
115
    this.handle = device;
72✔
116
    this.adapter = adapter;
72✔
117
    this.adapterInfo = adapterInfo;
72✔
118

119
    this.info = this._getInfo();
72✔
120
    this.features = this._getFeatures();
72✔
121
    this.limits = getWebGPUDeviceLimits(this.handle.limits);
72✔
122

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

133
    // "Context" loss handling
134
    this.lost = this.handle.lost.then(lostInfo => {
72✔
135
      this._isLost = true;
8✔
136
      return {reason: 'destroyed', message: lostInfo.message};
8✔
137
    });
138

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

147
    this.commandEncoder = this.createCommandEncoder({});
72✔
148
  }
149

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

155
  destroy(): void {
156
    if (!this._releaseDeviceReference()) {
1!
NEW
157
      return;
×
158
    }
159

160
    this._finalizeDevice({destroyHandle: this._ownsHandle});
1✔
161
  }
162

163
  override detach(): GPUDevice {
NEW
164
    this._detachDeviceReference();
×
NEW
165
    this._finalizeDevice({destroyHandle: false});
×
NEW
166
    return this.handle;
×
167
  }
168

169
  private _finalizeDevice(options: {destroyHandle: boolean}): void {
170
    this._destroyCanvasSurfaces();
1✔
171
    this.commandEncoder?.destroy();
1✔
172
    this._defaultSampler?.destroy();
1✔
173
    this._defaultSampler = null;
1✔
174
    if (options.destroyHandle) {
1!
175
      this.handle.destroy();
1✔
176
    }
177
  }
178

179
  get isLost(): boolean {
180
    return this._isLost;
288✔
181
  }
182

183
  getShaderLayout(source: string) {
184
    return getShaderLayoutFromWGSL(source);
688✔
185
  }
186

187
  override isVertexFormatSupported(format: VertexFormat): boolean {
188
    const info = this.getVertexFormatInfo(format);
×
189
    return !info.webglOnly;
×
190
  }
191

192
  createBuffer(props: BufferProps | ArrayBuffer | ArrayBufferView): WebGPUBuffer {
193
    const newProps = this._normalizeBufferProps(props);
1,241✔
194
    return new WebGPUBuffer(this, newProps);
1,241✔
195
  }
196

197
  createTexture(props: TextureProps): WebGPUTexture {
198
    return new WebGPUTexture(this, props);
296✔
199
  }
200

201
  createExternalTexture(props: ExternalTextureProps): WebGPUExternalTexture {
202
    return new WebGPUExternalTexture(this, props);
1✔
203
  }
204

205
  createShader(props: ShaderProps): WebGPUShader {
206
    return new WebGPUShader(this, props);
397✔
207
  }
208

209
  createSampler(props: SamplerProps): WebGPUSampler {
210
    return new WebGPUSampler(this, props as WebGPUSamplerProps);
13✔
211
  }
212

213
  getDefaultSampler(): WebGPUSampler {
214
    this._defaultSampler ||= new WebGPUSampler(this, {
223✔
215
      id: `${this.id}-default-sampler`
216
    });
217
    return this._defaultSampler;
223✔
218
  }
219

220
  createRenderPipeline(props: RenderPipelineProps): WebGPURenderPipeline {
221
    return new WebGPURenderPipeline(this, props);
84✔
222
  }
223

224
  createFramebuffer(props: FramebufferProps): WebGPUFramebuffer {
225
    return new WebGPUFramebuffer(this, props);
137✔
226
  }
227

228
  createComputePipeline(props: ComputePipelineProps): WebGPUComputePipeline {
229
    return new WebGPUComputePipeline(this, props);
284✔
230
  }
231

232
  /** Creates an encoder for reusable WebGPU draw commands. */
233
  createRenderBundleEncoder(props: RenderBundleEncoderProps = {}): WebGPURenderBundleEncoder {
×
234
    return new WebGPURenderBundleEncoder(this, props);
5✔
235
  }
236

237
  createVertexArray(props: VertexArrayProps): WebGPUVertexArray {
238
    return new WebGPUVertexArray(this, props);
88✔
239
  }
240

241
  override createCommandEncoder(props?: CommandEncoderProps): WebGPUCommandEncoder {
242
    return new WebGPUCommandEncoder(this, props);
370✔
243
  }
244

245
  override writeBufferViaCommandEncoder(
246
    commandEncoder: CommandEncoder,
247
    destinationBuffer: Buffer,
248
    data: ArrayBufferLike | ArrayBufferView | SharedArrayBuffer,
249
    byteOffset: number = 0
52✔
250
  ): void {
251
    const webgpuCommandEncoder = commandEncoder as WebGPUCommandEncoder;
52✔
252
    const uploadData = ArrayBuffer.isView(data)
52!
253
      ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
254
      : new Uint8Array(data);
255
    // WebGPU cannot encode CPU writes directly on a command encoder. Record the
256
    // upload as a staging-buffer copy so it stays ordered with the draw/dispatch
257
    // commands that will consume the destination buffer.
258
    const uploadBuffer = this.createBuffer({
52✔
259
      usage: Buffer.COPY_SRC,
260
      data: uploadData
261
    });
262

263
    webgpuCommandEncoder.trackTransientUploadBuffer(uploadBuffer);
52✔
264
    webgpuCommandEncoder.copyBufferToBuffer({
52✔
265
      sourceBuffer: uploadBuffer,
266
      destinationBuffer,
267
      destinationOffset: byteOffset,
268
      size: uploadData.byteLength
269
    });
270
  }
271

272
  // WebGPU specifics
273

274
  createTransformFeedback(props: TransformFeedbackProps): TransformFeedback {
275
    throw new Error('Transform feedback not supported in WebGPU');
×
276
  }
277

278
  override createQuerySet(props: QuerySetProps): QuerySet {
279
    return new WebGPUQuerySet(this, props);
6✔
280
  }
281

282
  override createFence(): WebGPUFence {
283
    return new WebGPUFence(this);
1✔
284
  }
285

286
  createCanvasContext(props: CanvasContextProps): WebGPUCanvasContext {
287
    return this._registerCanvasSurface(new WebGPUCanvasContext(this, this.adapter, props));
1✔
288
  }
289

290
  createPresentationContext(props?: PresentationContextProps): PresentationContext {
291
    return this._registerCanvasSurface(new WebGPUPresentationContext(this, props));
3✔
292
  }
293

294
  createPipelineLayout(props: PipelineLayoutProps): WebGPUPipelineLayout {
295
    return new WebGPUPipelineLayout(this, props);
84✔
296
  }
297

298
  override generateMipmapsWebGPU(texture: Texture): void {
299
    generateMipmapsWebGPU(this, texture);
12✔
300
  }
301

302
  override _createBindGroupLayoutWebGPU(
303
    pipeline: RenderPipeline | ComputePipeline,
304
    group: number
305
  ): GPUBindGroupLayout {
306
    return (pipeline as WebGPURenderPipeline | WebGPUComputePipeline).handle.getBindGroupLayout(
337✔
307
      group
308
    );
309
  }
310

311
  override _createBindGroupWebGPU(
312
    bindGroupLayout: unknown,
313
    shaderLayout: ShaderLayout | ComputeShaderLayout,
314
    bindings: Bindings,
315
    group: number,
316
    label?: string
317
  ): GPUBindGroup | null {
318
    if (Object.keys(bindings).length === 0) {
712✔
319
      return this.handle.createBindGroup({
18✔
320
        label,
321
        layout: bindGroupLayout as GPUBindGroupLayout,
322
        entries: []
323
      });
324
    }
325

326
    return getBindGroup(
694✔
327
      this,
328
      bindGroupLayout as GPUBindGroupLayout,
329
      shaderLayout,
330
      bindings,
331
      group,
332
      label
333
    );
334
  }
335

336
  submit(commandBuffer?: WebGPUCommandBuffer): void {
337
    let submittedCommandEncoder: WebGPUCommandEncoder | null = null;
284✔
338
    if (!commandBuffer) {
284✔
339
      ({submittedCommandEncoder, commandBuffer} = this._finalizeDefaultCommandEncoderForSubmit());
213✔
340
    }
341

342
    const profiler = getWebGPUCpuHotspotProfiler(this);
284✔
343
    const startTime = profiler ? getTimestamp() : 0;
284✔
344
    const submitReason = getWebGPUCpuHotspotSubmitReason(this);
284✔
345
    const transientUploadBuffers = commandBuffer.transientUploadBuffers;
284✔
346
    let didSubmit = false;
284✔
347
    try {
284✔
348
      this.pushErrorScope('validation');
284✔
349
      const queueSubmitStartTime = profiler ? getTimestamp() : 0;
284✔
350
      this.handle.queue.submit([commandBuffer.handle]);
284✔
351
      didSubmit = true;
284✔
352
      if (profiler) {
284✔
353
        profiler.queueSubmitCount = (profiler.queueSubmitCount || 0) + 1;
42✔
354
        profiler.queueSubmitTimeMs =
42✔
355
          (profiler.queueSubmitTimeMs || 0) + (getTimestamp() - queueSubmitStartTime);
56✔
356
      }
357
      this.popErrorScope((error: GPUError) => {
284✔
358
        this.reportError(new Error(`${this} command submission: ${error.message}`), this)();
1✔
359
        this.debug();
1✔
360
      });
361

362
      if (submittedCommandEncoder) {
284✔
363
        const submitResolveKickoffStartTime = profiler ? getTimestamp() : 0;
213✔
364
        scheduleMicrotask(() => {
213✔
365
          submittedCommandEncoder
213✔
366
            .resolveTimeProfilingQuerySet()
367
            .then(() => {
368
              this.commandEncoder._gpuTimeMs = submittedCommandEncoder._gpuTimeMs;
213✔
369
            })
370
            .catch(() => {});
371
        });
372
        if (profiler) {
213✔
373
          profiler.submitResolveKickoffCount = (profiler.submitResolveKickoffCount || 0) + 1;
42✔
374
          profiler.submitResolveKickoffTimeMs =
42✔
375
            (profiler.submitResolveKickoffTimeMs || 0) +
84✔
376
            (getTimestamp() - submitResolveKickoffStartTime);
377
        }
378
      }
379
    } finally {
380
      if (transientUploadBuffers.length) {
284✔
381
        if (didSubmit) {
5!
382
          // The GPU may still be reading from the staging buffers after
383
          // queue.submit() returns, so defer destruction until the submitted
384
          // work has fully completed.
385
          this.handle.queue
5✔
386
            .onSubmittedWorkDone()
387
            .then(() => {
388
              for (const uploadBuffer of transientUploadBuffers) {
4✔
389
                uploadBuffer.destroy();
10✔
390
              }
391
            })
392
            .catch(() => {
393
              for (const uploadBuffer of transientUploadBuffers) {
×
394
                uploadBuffer.destroy();
×
395
              }
396
            });
397
        } else {
398
          for (const uploadBuffer of transientUploadBuffers) {
×
399
            uploadBuffer.destroy();
×
400
          }
401
        }
402
      }
403
      if (profiler) {
284✔
404
        profiler.submitCount = (profiler.submitCount || 0) + 1;
42✔
405
        profiler.submitTimeMs = (profiler.submitTimeMs || 0) + (getTimestamp() - startTime);
42✔
406
        const reasonCountKey =
407
          submitReason === 'query-readback' ? 'queryReadbackSubmitCount' : 'defaultSubmitCount';
42!
408
        const reasonTimeKey =
409
          submitReason === 'query-readback' ? 'queryReadbackSubmitTimeMs' : 'defaultSubmitTimeMs';
42!
410
        profiler[reasonCountKey] = (profiler[reasonCountKey] || 0) + 1;
42✔
411
        profiler[reasonTimeKey] = (profiler[reasonTimeKey] || 0) + (getTimestamp() - startTime);
42✔
412
      }
413
      const commandBufferDestroyStartTime = profiler ? getTimestamp() : 0;
284✔
414
      commandBuffer.destroy();
284✔
415
      if (profiler) {
284✔
416
        profiler.commandBufferDestroyCount = (profiler.commandBufferDestroyCount || 0) + 1;
42✔
417
        profiler.commandBufferDestroyTimeMs =
42✔
418
          (profiler.commandBufferDestroyTimeMs || 0) +
46✔
419
          (getTimestamp() - commandBufferDestroyStartTime);
420
      }
421
    }
422
  }
423

424
  private _finalizeDefaultCommandEncoderForSubmit(): {
425
    submittedCommandEncoder: WebGPUCommandEncoder;
426
    commandBuffer: WebGPUCommandBuffer;
427
  } {
428
    const submittedCommandEncoder = this.commandEncoder;
213✔
429
    if (
213!
430
      submittedCommandEncoder.getTimeProfilingSlotCount() > 0 &&
213!
431
      submittedCommandEncoder.getTimeProfilingQuerySet() instanceof WebGPUQuerySet
432
    ) {
433
      const querySet = submittedCommandEncoder.getTimeProfilingQuerySet() as WebGPUQuerySet;
×
434
      querySet._encodeResolveToReadBuffer(submittedCommandEncoder, {
×
435
        firstQuery: 0,
436
        queryCount: submittedCommandEncoder.getTimeProfilingSlotCount()
437
      });
438
    }
439

440
    const commandBuffer = submittedCommandEncoder.finish();
213✔
441
    this.commandEncoder.destroy();
213✔
442
    this.commandEncoder = this.createCommandEncoder({
213✔
443
      id: submittedCommandEncoder.props.id,
444
      timeProfilingQuerySet: submittedCommandEncoder.getTimeProfilingQuerySet()
445
    });
446

447
    return {submittedCommandEncoder, commandBuffer};
213✔
448
  }
449

450
  // WebGPU specific
451

452
  pushErrorScope(scope: 'validation' | 'out-of-memory'): void {
453
    if (!this.props.debug) {
8,136✔
454
      return;
18✔
455
    }
456
    const profiler = getWebGPUCpuHotspotProfiler(this);
8,118✔
457
    const startTime = profiler ? getTimestamp() : 0;
8,118✔
458
    this.handle.pushErrorScope(scope);
8,118✔
459
    if (profiler) {
8,118✔
460
      profiler.errorScopePushCount = (profiler.errorScopePushCount || 0) + 1;
149✔
461
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
149✔
462
    }
463
  }
464

465
  popErrorScope(handler: (error: GPUError) => void): Promise<void> {
466
    if (!this.props.debug) {
8,136✔
467
      return Promise.resolve();
18✔
468
    }
469
    const profiler = getWebGPUCpuHotspotProfiler(this);
8,118✔
470
    const startTime = profiler ? getTimestamp() : 0;
8,118✔
471
    const errorScopePromise = this.handle
8,118✔
472
      .popErrorScope()
473
      .then((error: GPUError | null) => {
474
        if (error) {
7,109✔
475
          handler(error);
11✔
476
        }
477
      })
478
      .catch((error: unknown) => {
479
        if (this.shouldIgnoreDroppedInstanceError(error, 'popErrorScope')) {
209!
480
          return;
209✔
481
        }
482

483
        const errorMessage = error instanceof Error ? error.message : String(error);
×
484
        this.reportError(new Error(`${this} popErrorScope failed: ${errorMessage}`), this)();
×
485
        this.debug();
×
486
      });
487
    if (profiler) {
8,118✔
488
      profiler.errorScopePopCount = (profiler.errorScopePopCount || 0) + 1;
149✔
489
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
149✔
490
    }
491
    return errorScopePromise;
8,118✔
492
  }
493

494
  // PRIVATE METHODS
495

496
  protected _getInfo(): DeviceInfo {
497
    const [driver, driverVersion] = ((this.adapterInfo as any).driver || '').split(' Version ');
72✔
498

499
    // See https://developer.chrome.com/blog/new-in-webgpu-120#adapter_information_updates
500
    const vendor = this.adapterInfo.vendor || this.adapter.__brand || 'unknown';
72!
501
    const renderer = driver || '';
72✔
502
    const version = driverVersion || '';
72✔
503
    const fallback = Boolean(
72✔
504
      (this.adapterInfo as any).isFallbackAdapter ??
72!
505
        (this.adapter as any).isFallbackAdapter ??
506
        false
507
    );
508
    const softwareRenderer = /SwiftShader/i.test(
72✔
509
      `${vendor} ${renderer} ${this.adapterInfo.architecture || ''}`
72!
510
    );
511

512
    const gpuArchitecture = this.adapterInfo.architecture || 'unknown';
72!
513
    const gpu =
514
      identifyGPUVendor(vendor, renderer) ??
72✔
515
      (softwareRenderer || fallback ? 'software' : 'unknown');
144!
516
    const gpuBackend = (this.adapterInfo as any).backend || 'unknown';
72✔
517
    const gpuType =
72✔
518
      ((this.adapterInfo as any).type || '').split(' ')[0].toLowerCase() ||
144✔
519
      (softwareRenderer || fallback ? 'cpu' : 'unknown');
144!
520

521
    return {
72✔
522
      type: 'webgpu',
523
      vendor,
524
      renderer,
525
      version,
526
      gpu,
527
      gpuType,
528
      gpuBackend,
529
      gpuArchitecture,
530
      fallback,
531
      featureLevel: getWebGPUDeviceFeatureLevel(this.props.featureLevel),
532
      shadingLanguage: 'wgsl',
533
      shadingLanguageVersion: 100
534
    };
535
  }
536

537
  shouldIgnoreDroppedInstanceError(error: unknown, operation?: string): boolean {
538
    const errorMessage = error instanceof Error ? error.message : String(error);
213!
539
    return (
213✔
540
      errorMessage.includes('Instance dropped') &&
852!
541
      (!operation || errorMessage.includes(operation)) &&
542
      (this._isLost ||
543
        this.info.gpu === 'software' ||
544
        this.info.gpuType === 'cpu' ||
545
        Boolean(this.info.fallback))
546
    );
547
  }
548

549
  protected _getFeatures(): DeviceFeatures {
550
    // Initialize with actual WebGPU Features (note that unknown features may not be in DeviceFeature type)
551
    const features = new Set<DeviceFeature>(this.handle.features as Set<DeviceFeature>);
72✔
552
    // Fixups for pre-standard names: https://github.com/webgpu-native/webgpu-headers/issues/133
553
    // @ts-expect-error Chrome Canary v99
554
    if (features.has('depth-clamping')) {
72!
555
      // @ts-expect-error Chrome Canary v99
556
      features.delete('depth-clamping');
×
557
      features.add('depth-clip-control');
×
558
    }
559

560
    // Some subsets of WebGPU extensions correspond to WebGL extensions
561
    if (features.has('texture-compression-bc')) {
72✔
562
      features.add('texture-compression-bc5-webgl');
69✔
563
    }
564

565
    if (this.handle.features.has('chromium-experimental-norm16-texture-formats')) {
72!
566
      features.add('norm16-renderable-webgl');
×
567
    }
568

569
    if (this.handle.features.has('chromium-experimental-snorm16-texture-formats')) {
72!
570
      features.add('snorm16-renderable-webgl');
×
571
    }
572

573
    const WEBGPU_ALWAYS_FEATURES: DeviceFeature[] = [
72✔
574
      'compilation-status-async-webgl',
575
      'float32-renderable-webgl',
576
      'float16-renderable-webgl',
577
      'norm16-renderable-webgl',
578
      'texture-filterable-anisotropic-webgl',
579
      'shader-noperspective-interpolation-webgl'
580
    ];
581

582
    for (const feature of WEBGPU_ALWAYS_FEATURES) {
72✔
583
      features.add(feature);
432✔
584
    }
585

586
    if (
72!
587
      isHTMLInCanvasSupported() &&
72!
588
      typeof (
589
        this.handle.queue as GPUQueue & {
590
          copyElementImageToTexture?: unknown;
591
        }
592
      ).copyElementImageToTexture === 'function'
593
    ) {
594
      features.add('html-in-canvas');
×
595
    }
596

597
    return new DeviceFeatures(Array.from(features), this.props._disabledFeatures);
72✔
598
  }
599

600
  override _getDeviceSpecificTextureFormatCapabilities(
601
    capabilities: DeviceTextureFormatCapabilities
602
  ): DeviceTextureFormatCapabilities {
603
    const {format} = capabilities;
76✔
604
    if (format.includes('webgl')) {
76✔
605
      return {format, create: false, render: false, filter: false, blend: false, store: false};
11✔
606
    }
607
    return capabilities;
65✔
608
  }
609
}
610

611
function getWebGPUDeviceLimits(limits: GPUSupportedLimits): DeviceLimits {
612
  const stageSpecificLimits: Partial<Record<keyof DeviceLimits, number>> = {
72✔
613
    maxStorageBuffersInVertexStage:
614
      limits.maxStorageBuffersInVertexStage ?? limits.maxStorageBuffersPerShaderStage,
72!
615
    maxStorageBuffersInFragmentStage:
616
      limits.maxStorageBuffersInFragmentStage ?? limits.maxStorageBuffersPerShaderStage,
72!
617
    maxStorageTexturesInVertexStage:
618
      limits.maxStorageTexturesInVertexStage ?? limits.maxStorageTexturesPerShaderStage,
72!
619
    maxStorageTexturesInFragmentStage:
620
      limits.maxStorageTexturesInFragmentStage ?? limits.maxStorageTexturesPerShaderStage
72!
621
  };
622

623
  return new Proxy(limits, {
72✔
624
    get(target, property) {
625
      return typeof property === 'string' && property in stageSpecificLimits
1,202✔
626
        ? stageSpecificLimits[property as keyof DeviceLimits]
627
        : Reflect.get(target, property, target);
628
    }
629
  }) as DeviceLimits;
630
}
631

632
function getWebGPUDeviceFeatureLevel(
633
  featureLevel: DeviceProps['featureLevel']
634
): NonNullable<DeviceInfo['featureLevel']> {
635
  return featureLevel === 'best-available' ? 'core' : featureLevel || 'core';
72!
636
}
637

638
function identifyGPUVendor(
639
  vendor: string,
640
  renderer: string
641
): 'nvidia' | 'intel' | 'apple' | 'amd' | null {
642
  if (/NVIDIA/i.exec(vendor) || /NVIDIA/i.exec(renderer)) {
72!
643
    return 'nvidia';
×
644
  }
645
  if (/INTEL/i.exec(vendor) || /INTEL/i.exec(renderer)) {
72!
646
    return 'intel';
×
647
  }
648
  if (/Apple/i.exec(vendor) || /Apple/i.exec(renderer)) {
72!
649
    return 'apple';
×
650
  }
651
  if (
72!
652
    /AMD/i.exec(vendor) ||
288✔
653
    /AMD/i.exec(renderer) ||
654
    /ATI/i.exec(vendor) ||
655
    /ATI/i.exec(renderer)
656
  ) {
657
    return 'amd';
×
658
  }
659
  return null;
72✔
660
}
661

662
function scheduleMicrotask(callback: () => void): void {
663
  if (globalThis.queueMicrotask) {
213!
664
    globalThis.queueMicrotask(callback);
213✔
665
    return;
213✔
666
  }
667
  Promise.resolve()
×
668
    .then(callback)
669
    .catch(() => {});
670
}
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