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

visgl / luma.gl / 26158922031

20 May 2026 11:14AM UTC coverage: 74.85% (-0.02%) from 74.867%
26158922031

push

github

web-flow
fix(webgpu) gpu resolution in webgpu device info (#2631)

7337 of 11077 branches covered (66.24%)

Branch coverage included in aggregate %.

6 of 10 new or added lines in 1 file covered. (60.0%)

3 existing lines in 1 file now uncovered.

15969 of 20060 relevant lines covered (79.61%)

1015.1 hits per line

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

77.23
/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
  ShaderLayout
40
} from '@luma.gl/core';
41
import {Buffer, Device, DeviceFeatures} from '@luma.gl/core';
42
import {WebGPUBuffer} from './resources/webgpu-buffer';
43
import {WebGPUTexture} from './resources/webgpu-texture';
44
import {WebGPUExternalTexture} from './resources/webgpu-external-texture';
45
import {WebGPUSampler} from './resources/webgpu-sampler';
46
import {WebGPUShader} from './resources/webgpu-shader';
47
import {WebGPURenderPipeline} from './resources/webgpu-render-pipeline';
48
import {WebGPUFramebuffer} from './resources/webgpu-framebuffer';
49
import {WebGPUComputePipeline} from './resources/webgpu-compute-pipeline';
50
import {WebGPUVertexArray} from './resources/webgpu-vertex-array';
51

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

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

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

78
  /** type of this device */
79
  readonly type = 'webgpu';
39✔
80

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

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

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

92
  override canvasContext: WebGPUCanvasContext | null = null;
39✔
93

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

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

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

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

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

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

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

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

143
    this.commandEncoder = this.createCommandEncoder({});
39✔
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 {
152
    this.commandEncoder?.destroy();
×
153
    this._defaultSampler?.destroy();
×
154
    this._defaultSampler = null;
×
155
    this.handle.destroy();
×
156
  }
157

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

162
  getShaderLayout(source: string) {
163
    return getShaderLayoutFromWGSL(source);
66✔
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);
318✔
173
    return new WebGPUBuffer(this, newProps);
318✔
174
  }
175

176
  createTexture(props: TextureProps): WebGPUTexture {
177
    return new WebGPUTexture(this, props);
141✔
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);
90✔
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, {
111✔
194
      id: `${this.id}-default-sampler`
195
    });
196
    return this._defaultSampler;
111✔
197
  }
198

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

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

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

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

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

219
  override writeBufferViaCommandEncoder(
220
    commandEncoder: CommandEncoder,
221
    destinationBuffer: Buffer,
222
    data: ArrayBufferLike | ArrayBufferView | SharedArrayBuffer,
223
    byteOffset: number = 0
1✔
224
  ): void {
225
    const webgpuCommandEncoder = commandEncoder as WebGPUCommandEncoder;
1✔
226
    const uploadData = ArrayBuffer.isView(data)
1!
227
      ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
228
      : new Uint8Array(data);
229
    // WebGPU cannot encode CPU writes directly on a command encoder. Record the
230
    // upload as a staging-buffer copy so it stays ordered with the draw/dispatch
231
    // commands that will consume the destination buffer.
232
    const uploadBuffer = this.createBuffer({
1✔
233
      usage: Buffer.COPY_SRC,
234
      data: uploadData
235
    });
236

237
    webgpuCommandEncoder.trackTransientUploadBuffer(uploadBuffer);
1✔
238
    webgpuCommandEncoder.copyBufferToBuffer({
1✔
239
      sourceBuffer: uploadBuffer,
240
      destinationBuffer,
241
      destinationOffset: byteOffset,
242
      size: uploadData.byteLength
243
    });
244
  }
245

246
  // WebGPU specifics
247

248
  createTransformFeedback(props: TransformFeedbackProps): TransformFeedback {
249
    throw new Error('Transform feedback not supported in WebGPU');
×
250
  }
251

252
  override createQuerySet(props: QuerySetProps): QuerySet {
253
    return new WebGPUQuerySet(this, props);
6✔
254
  }
255

256
  override createFence(): WebGPUFence {
257
    return new WebGPUFence(this);
1✔
258
  }
259

260
  createCanvasContext(props: CanvasContextProps): WebGPUCanvasContext {
261
    return new WebGPUCanvasContext(this, this.adapter, props);
×
262
  }
263

264
  createPresentationContext(props?: PresentationContextProps): PresentationContext {
265
    return new WebGPUPresentationContext(this, props);
2✔
266
  }
267

268
  createPipelineLayout(props: PipelineLayoutProps): WebGPUPipelineLayout {
269
    return new WebGPUPipelineLayout(this, props);
24✔
270
  }
271

272
  override generateMipmapsWebGPU(texture: Texture): void {
273
    generateMipmapsWebGPU(this, texture);
12✔
274
  }
275

276
  override _createBindGroupLayoutWebGPU(
277
    pipeline: RenderPipeline | ComputePipeline,
278
    group: number
279
  ): GPUBindGroupLayout {
280
    return (pipeline as WebGPURenderPipeline | WebGPUComputePipeline).handle.getBindGroupLayout(
50✔
281
      group
282
    );
283
  }
284

285
  override _createBindGroupWebGPU(
286
    bindGroupLayout: unknown,
287
    shaderLayout: ShaderLayout | ComputeShaderLayout,
288
    bindings: Bindings,
289
    group: number,
290
    label?: string
291
  ): GPUBindGroup | null {
292
    if (Object.keys(bindings).length === 0) {
86✔
293
      return this.handle.createBindGroup({
7✔
294
        label,
295
        layout: bindGroupLayout as GPUBindGroupLayout,
296
        entries: []
297
      });
298
    }
299

300
    return getBindGroup(
79✔
301
      this,
302
      bindGroupLayout as GPUBindGroupLayout,
303
      shaderLayout,
304
      bindings,
305
      group,
306
      label
307
    );
308
  }
309

310
  submit(commandBuffer?: WebGPUCommandBuffer): void {
311
    let submittedCommandEncoder: WebGPUCommandEncoder | null = null;
127✔
312
    if (!commandBuffer) {
127✔
313
      ({submittedCommandEncoder, commandBuffer} = this._finalizeDefaultCommandEncoderForSubmit());
122✔
314
    }
315

316
    const profiler = getWebGPUCpuHotspotProfiler(this);
127✔
317
    const startTime = profiler ? getTimestamp() : 0;
127✔
318
    const submitReason = getWebGPUCpuHotspotSubmitReason(this);
127✔
319
    const transientUploadBuffers = commandBuffer.transientUploadBuffers;
127✔
320
    let didSubmit = false;
127✔
321
    try {
127✔
322
      this.pushErrorScope('validation');
127✔
323
      const queueSubmitStartTime = profiler ? getTimestamp() : 0;
127✔
324
      this.handle.queue.submit([commandBuffer.handle]);
127✔
325
      didSubmit = true;
127✔
326
      if (profiler) {
127✔
327
        profiler.queueSubmitCount = (profiler.queueSubmitCount || 0) + 1;
42✔
328
        profiler.queueSubmitTimeMs =
42✔
329
          (profiler.queueSubmitTimeMs || 0) + (getTimestamp() - queueSubmitStartTime);
56✔
330
      }
331
      this.popErrorScope((error: GPUError) => {
127✔
332
        this.reportError(new Error(`${this} command submission: ${error.message}`), this)();
×
333
        this.debug();
×
334
      });
335

336
      if (submittedCommandEncoder) {
127✔
337
        const submitResolveKickoffStartTime = profiler ? getTimestamp() : 0;
122✔
338
        scheduleMicrotask(() => {
122✔
339
          submittedCommandEncoder
122✔
340
            .resolveTimeProfilingQuerySet()
341
            .then(() => {
342
              this.commandEncoder._gpuTimeMs = submittedCommandEncoder._gpuTimeMs;
122✔
343
            })
344
            .catch(() => {});
345
        });
346
        if (profiler) {
122✔
347
          profiler.submitResolveKickoffCount = (profiler.submitResolveKickoffCount || 0) + 1;
42✔
348
          profiler.submitResolveKickoffTimeMs =
42✔
349
            (profiler.submitResolveKickoffTimeMs || 0) +
48✔
350
            (getTimestamp() - submitResolveKickoffStartTime);
351
        }
352
      }
353
    } finally {
354
      if (transientUploadBuffers.length) {
127!
355
        if (didSubmit) {
×
356
          // The GPU may still be reading from the staging buffers after
357
          // queue.submit() returns, so defer destruction until the submitted
358
          // work has fully completed.
359
          this.handle.queue
×
360
            .onSubmittedWorkDone()
361
            .then(() => {
362
              for (const uploadBuffer of transientUploadBuffers) {
×
363
                uploadBuffer.destroy();
×
364
              }
365
            })
366
            .catch(() => {
367
              for (const uploadBuffer of transientUploadBuffers) {
×
368
                uploadBuffer.destroy();
×
369
              }
370
            });
371
        } else {
372
          for (const uploadBuffer of transientUploadBuffers) {
×
373
            uploadBuffer.destroy();
×
374
          }
375
        }
376
      }
377
      if (profiler) {
127✔
378
        profiler.submitCount = (profiler.submitCount || 0) + 1;
42✔
379
        profiler.submitTimeMs = (profiler.submitTimeMs || 0) + (getTimestamp() - startTime);
42✔
380
        const reasonCountKey =
381
          submitReason === 'query-readback' ? 'queryReadbackSubmitCount' : 'defaultSubmitCount';
42!
382
        const reasonTimeKey =
383
          submitReason === 'query-readback' ? 'queryReadbackSubmitTimeMs' : 'defaultSubmitTimeMs';
42!
384
        profiler[reasonCountKey] = (profiler[reasonCountKey] || 0) + 1;
42✔
385
        profiler[reasonTimeKey] = (profiler[reasonTimeKey] || 0) + (getTimestamp() - startTime);
42✔
386
      }
387
      const commandBufferDestroyStartTime = profiler ? getTimestamp() : 0;
127✔
388
      commandBuffer.destroy();
127✔
389
      if (profiler) {
127✔
390
        profiler.commandBufferDestroyCount = (profiler.commandBufferDestroyCount || 0) + 1;
42✔
391
        profiler.commandBufferDestroyTimeMs =
42✔
392
          (profiler.commandBufferDestroyTimeMs || 0) +
50✔
393
          (getTimestamp() - commandBufferDestroyStartTime);
394
      }
395
    }
396
  }
397

398
  private _finalizeDefaultCommandEncoderForSubmit(): {
399
    submittedCommandEncoder: WebGPUCommandEncoder;
400
    commandBuffer: WebGPUCommandBuffer;
401
  } {
402
    const submittedCommandEncoder = this.commandEncoder;
122✔
403
    if (
122!
404
      submittedCommandEncoder.getTimeProfilingSlotCount() > 0 &&
122!
405
      submittedCommandEncoder.getTimeProfilingQuerySet() instanceof WebGPUQuerySet
406
    ) {
407
      const querySet = submittedCommandEncoder.getTimeProfilingQuerySet() as WebGPUQuerySet;
×
408
      querySet._encodeResolveToReadBuffer(submittedCommandEncoder, {
×
409
        firstQuery: 0,
410
        queryCount: submittedCommandEncoder.getTimeProfilingSlotCount()
411
      });
412
    }
413

414
    const commandBuffer = submittedCommandEncoder.finish();
122✔
415
    this.commandEncoder.destroy();
122✔
416
    this.commandEncoder = this.createCommandEncoder({
122✔
417
      id: submittedCommandEncoder.props.id,
418
      timeProfilingQuerySet: submittedCommandEncoder.getTimeProfilingQuerySet()
419
    });
420

421
    return {submittedCommandEncoder, commandBuffer};
122✔
422
  }
423

424
  // WebGPU specific
425

426
  pushErrorScope(scope: 'validation' | 'out-of-memory'): void {
427
    if (!this.props.debug) {
2,596!
428
      return;
×
429
    }
430
    const profiler = getWebGPUCpuHotspotProfiler(this);
2,596✔
431
    const startTime = profiler ? getTimestamp() : 0;
2,596✔
432
    this.handle.pushErrorScope(scope);
2,596✔
433
    if (profiler) {
2,596✔
434
      profiler.errorScopePushCount = (profiler.errorScopePushCount || 0) + 1;
149✔
435
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
149✔
436
    }
437
  }
438

439
  popErrorScope(handler: (error: GPUError) => void): void {
440
    if (!this.props.debug) {
2,596!
441
      return;
×
442
    }
443
    const profiler = getWebGPUCpuHotspotProfiler(this);
2,596✔
444
    const startTime = profiler ? getTimestamp() : 0;
2,596✔
445
    this.handle
2,596✔
446
      .popErrorScope()
447
      .then((error: GPUError | null) => {
448
        if (error) {
2,206✔
449
          handler(error);
11✔
450
        }
451
      })
452
      .catch((error: unknown) => {
453
        if (this.shouldIgnoreDroppedInstanceError(error, 'popErrorScope')) {
390!
454
          return;
390✔
455
        }
456

457
        const errorMessage = error instanceof Error ? error.message : String(error);
×
458
        this.reportError(new Error(`${this} popErrorScope failed: ${errorMessage}`), this)();
390✔
459
        this.debug();
390✔
460
      });
461
    if (profiler) {
2,596✔
462
      profiler.errorScopePopCount = (profiler.errorScopePopCount || 0) + 1;
149✔
463
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
149✔
464
    }
465
  }
466

467
  // PRIVATE METHODS
468

469
  protected _getInfo(): DeviceInfo {
470
    const [driver, driverVersion] = ((this.adapterInfo as any).driver || '').split(' Version ');
39✔
471

472
    // See https://developer.chrome.com/blog/new-in-webgpu-120#adapter_information_updates
473
    const vendor = this.adapterInfo.vendor || this.adapter.__brand || 'unknown';
39!
474
    const renderer = driver || '';
39✔
475
    const version = driverVersion || '';
39✔
476
    const fallback = Boolean(
39✔
477
      (this.adapterInfo as any).isFallbackAdapter ??
39!
478
        (this.adapter as any).isFallbackAdapter ??
479
        false
480
    );
481
    const softwareRenderer = /SwiftShader/i.test(
39✔
482
      `${vendor} ${renderer} ${this.adapterInfo.architecture || ''}`
39!
483
    );
484

485
    const gpuArchitecture = this.adapterInfo.architecture || 'unknown';
39!
486
    const gpu =
487
      identifyGPUVendor(vendor, renderer) ??
39✔
488
      (softwareRenderer || fallback ? 'software' : 'unknown');
78!
489
    const gpuBackend = (this.adapterInfo as any).backend || 'unknown';
39✔
490
    const gpuType =
39✔
491
      ((this.adapterInfo as any).type || '').split(' ')[0].toLowerCase() ||
78✔
492
      (softwareRenderer || fallback ? 'cpu' : 'unknown');
78!
493

494
    return {
39✔
495
      type: 'webgpu',
496
      vendor,
497
      renderer,
498
      version,
499
      gpu,
500
      gpuType,
501
      gpuBackend,
502
      gpuArchitecture,
503
      fallback,
504
      shadingLanguage: 'wgsl',
505
      shadingLanguageVersion: 100
506
    };
507
  }
508

509
  shouldIgnoreDroppedInstanceError(error: unknown, operation?: string): boolean {
510
    const errorMessage = error instanceof Error ? error.message : String(error);
394!
511
    return (
394✔
512
      errorMessage.includes('Instance dropped') &&
1,576!
513
      (!operation || errorMessage.includes(operation)) &&
514
      (this._isLost ||
515
        this.info.gpu === 'software' ||
516
        this.info.gpuType === 'cpu' ||
517
        Boolean(this.info.fallback))
518
    );
519
  }
520

521
  protected _getFeatures(): DeviceFeatures {
522
    // Initialize with actual WebGPU Features (note that unknown features may not be in DeviceFeature type)
523
    const features = new Set<DeviceFeature>(this.handle.features as Set<DeviceFeature>);
39✔
524
    // Fixups for pre-standard names: https://github.com/webgpu-native/webgpu-headers/issues/133
525
    // @ts-expect-error Chrome Canary v99
526
    if (features.has('depth-clamping')) {
39!
527
      // @ts-expect-error Chrome Canary v99
528
      features.delete('depth-clamping');
×
529
      features.add('depth-clip-control');
×
530
    }
531

532
    // Some subsets of WebGPU extensions correspond to WebGL extensions
533
    if (features.has('texture-compression-bc')) {
39!
534
      features.add('texture-compression-bc5-webgl');
39✔
535
    }
536

537
    if (this.handle.features.has('chromium-experimental-norm16-texture-formats')) {
39!
538
      features.add('norm16-renderable-webgl');
×
539
    }
540

541
    if (this.handle.features.has('chromium-experimental-snorm16-texture-formats')) {
39!
542
      features.add('snorm16-renderable-webgl');
×
543
    }
544

545
    const WEBGPU_ALWAYS_FEATURES: DeviceFeature[] = [
39✔
546
      'compilation-status-async-webgl',
547
      'float32-renderable-webgl',
548
      'float16-renderable-webgl',
549
      'norm16-renderable-webgl',
550
      'texture-filterable-anisotropic-webgl',
551
      'shader-noperspective-interpolation-webgl'
552
    ];
553

554
    for (const feature of WEBGPU_ALWAYS_FEATURES) {
39✔
555
      features.add(feature);
234✔
556
    }
557

558
    return new DeviceFeatures(Array.from(features), this.props._disabledFeatures);
39✔
559
  }
560

561
  override _getDeviceSpecificTextureFormatCapabilities(
562
    capabilities: DeviceTextureFormatCapabilities
563
  ): DeviceTextureFormatCapabilities {
564
    const {format} = capabilities;
72✔
565
    if (format.includes('webgl')) {
72✔
566
      return {format, create: false, render: false, filter: false, blend: false, store: false};
11✔
567
    }
568
    return capabilities;
61✔
569
  }
570
}
571

572
function identifyGPUVendor(
573
  vendor: string,
574
  renderer: string
575
): 'nvidia' | 'intel' | 'apple' | 'amd' | null {
576
  if (/NVIDIA/i.exec(vendor) || /NVIDIA/i.exec(renderer)) {
39!
NEW
577
    return 'nvidia';
×
578
  }
579
  if (/INTEL/i.exec(vendor) || /INTEL/i.exec(renderer)) {
39!
NEW
580
    return 'intel';
×
581
  }
582
  if (/Apple/i.exec(vendor) || /Apple/i.exec(renderer)) {
39!
NEW
583
    return 'apple';
×
584
  }
585
  if (
39!
586
    /AMD/i.exec(vendor) ||
156✔
587
    /AMD/i.exec(renderer) ||
588
    /ATI/i.exec(vendor) ||
589
    /ATI/i.exec(renderer)
590
  ) {
NEW
591
    return 'amd';
×
592
  }
593
  return null;
39✔
594
}
595

596
function scheduleMicrotask(callback: () => void): void {
597
  if (globalThis.queueMicrotask) {
122!
598
    globalThis.queueMicrotask(callback);
122✔
599
    return;
122✔
600
  }
601
  Promise.resolve()
×
602
    .then(callback)
603
    .catch(() => {});
604
}
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