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

visgl / luma.gl / 28112330114

24 Jun 2026 04:07PM UTC coverage: 70.649% (-0.03%) from 70.68%
28112330114

push

github

web-flow
[codex] Move render draw state to render passes (#2689)

10077 of 16053 branches covered (62.77%)

Branch coverage included in aggregate %.

97 of 112 new or added lines in 8 files covered. (86.61%)

14 existing lines in 5 files now uncovered.

20430 of 27128 relevant lines covered (75.31%)

4011.66 hits per line

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

78.46
/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';
59✔
82

83
  readonly preferredColorFormat = navigator.gpu.getPreferredCanvasFormat() as
59✔
84
    | 'rgba8unorm'
85
    | 'bgra8unorm';
86
  readonly preferredDepthFormat = 'depth24plus';
59✔
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;
59✔
95

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

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

104
  override toString(): string {
105
    return `WebGPUDevice(${this.id})`;
×
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'});
59!
115
    this.handle = device;
59✔
116
    this.adapter = adapter;
59✔
117
    this.adapterInfo = adapterInfo;
59✔
118

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

123
    // Listen for uncaptured WebGPU errors
124
    device.addEventListener('uncapturederror', (event: Event) => {
59✔
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 => {
59✔
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);
59✔
141
    if (canvasContextProps) {
59!
142
      this.canvasContext = new WebGPUCanvasContext(this, this.adapter, canvasContextProps);
59✔
143
    }
144

145
    this.commandEncoder = this.createCommandEncoder({});
59✔
146
  }
147

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

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

160
  get isLost(): boolean {
161
    return this._isLost;
235✔
162
  }
163

164
  getShaderLayout(source: string) {
165
    return getShaderLayoutFromWGSL(source);
164✔
166
  }
167

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

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

178
  createTexture(props: TextureProps): WebGPUTexture {
179
    return new WebGPUTexture(this, props);
180✔
180
  }
181

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

186
  createShader(props: ShaderProps): WebGPUShader {
187
    return new WebGPUShader(this, props);
157✔
188
  }
189

190
  createSampler(props: SamplerProps): WebGPUSampler {
191
    return new WebGPUSampler(this, props as WebGPUSamplerProps);
7✔
192
  }
193

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

201
  createRenderPipeline(props: RenderPipelineProps): WebGPURenderPipeline {
202
    return new WebGPURenderPipeline(this, props);
54✔
203
  }
204

205
  createFramebuffer(props: FramebufferProps): WebGPUFramebuffer {
206
    return new WebGPUFramebuffer(this, props);
49✔
207
  }
208

209
  createComputePipeline(props: ComputePipelineProps): WebGPUComputePipeline {
210
    return new WebGPUComputePipeline(this, props);
89✔
211
  }
212

213
  /** Creates an encoder for reusable WebGPU draw commands. */
214
  createRenderBundleEncoder(props: RenderBundleEncoderProps = {}): WebGPURenderBundleEncoder {
4✔
215
    return new WebGPURenderBundleEncoder(this, props);
4✔
216
  }
217

218
  createVertexArray(props: VertexArrayProps): WebGPUVertexArray {
219
    return new WebGPUVertexArray(this, props);
49✔
220
  }
221

222
  override createCommandEncoder(props?: CommandEncoderProps): WebGPUCommandEncoder {
223
    return new WebGPUCommandEncoder(this, props);
270✔
224
  }
225

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

244
    webgpuCommandEncoder.trackTransientUploadBuffer(uploadBuffer);
7✔
245
    webgpuCommandEncoder.copyBufferToBuffer({
7✔
246
      sourceBuffer: uploadBuffer,
247
      destinationBuffer,
248
      destinationOffset: byteOffset,
249
      size: uploadData.byteLength
250
    });
251
  }
252

253
  // WebGPU specifics
254

255
  createTransformFeedback(props: TransformFeedbackProps): TransformFeedback {
256
    throw new Error('Transform feedback not supported in WebGPU');
×
257
  }
258

259
  override createQuerySet(props: QuerySetProps): QuerySet {
260
    return new WebGPUQuerySet(this, props);
6✔
261
  }
262

263
  override createFence(): WebGPUFence {
264
    return new WebGPUFence(this);
1✔
265
  }
266

267
  createCanvasContext(props: CanvasContextProps): WebGPUCanvasContext {
268
    return new WebGPUCanvasContext(this, this.adapter, props);
×
269
  }
270

271
  createPresentationContext(props?: PresentationContextProps): PresentationContext {
272
    return new WebGPUPresentationContext(this, props);
2✔
273
  }
274

275
  createPipelineLayout(props: PipelineLayoutProps): WebGPUPipelineLayout {
276
    return new WebGPUPipelineLayout(this, props);
54✔
277
  }
278

279
  override generateMipmapsWebGPU(texture: Texture): void {
280
    generateMipmapsWebGPU(this, texture);
12✔
281
  }
282

283
  override _createBindGroupLayoutWebGPU(
284
    pipeline: RenderPipeline | ComputePipeline,
285
    group: number
286
  ): GPUBindGroupLayout {
287
    return (pipeline as WebGPURenderPipeline | WebGPUComputePipeline).handle.getBindGroupLayout(
118✔
288
      group
289
    );
290
  }
291

292
  override _createBindGroupWebGPU(
293
    bindGroupLayout: unknown,
294
    shaderLayout: ShaderLayout | ComputeShaderLayout,
295
    bindings: Bindings,
296
    group: number,
297
    label?: string
298
  ): GPUBindGroup | null {
299
    if (Object.keys(bindings).length === 0) {
178✔
300
      return this.handle.createBindGroup({
16✔
301
        label,
302
        layout: bindGroupLayout as GPUBindGroupLayout,
303
        entries: []
304
      });
305
    }
306

307
    return getBindGroup(
162✔
308
      this,
309
      bindGroupLayout as GPUBindGroupLayout,
310
      shaderLayout,
311
      bindings,
312
      group,
313
      label
314
    );
315
  }
316

317
  submit(commandBuffer?: WebGPUCommandBuffer): void {
318
    let submittedCommandEncoder: WebGPUCommandEncoder | null = null;
206✔
319
    if (!commandBuffer) {
206✔
320
      ({submittedCommandEncoder, commandBuffer} = this._finalizeDefaultCommandEncoderForSubmit());
201✔
321
    }
322

323
    const profiler = getWebGPUCpuHotspotProfiler(this);
206✔
324
    const startTime = profiler ? getTimestamp() : 0;
206✔
325
    const submitReason = getWebGPUCpuHotspotSubmitReason(this);
206✔
326
    const transientUploadBuffers = commandBuffer.transientUploadBuffers;
206✔
327
    let didSubmit = false;
206✔
328
    try {
206✔
329
      this.pushErrorScope('validation');
206✔
330
      const queueSubmitStartTime = profiler ? getTimestamp() : 0;
206✔
331
      this.handle.queue.submit([commandBuffer.handle]);
206✔
332
      didSubmit = true;
206✔
333
      if (profiler) {
206✔
334
        profiler.queueSubmitCount = (profiler.queueSubmitCount || 0) + 1;
42✔
335
        profiler.queueSubmitTimeMs =
42✔
336
          (profiler.queueSubmitTimeMs || 0) + (getTimestamp() - queueSubmitStartTime);
62✔
337
      }
338
      this.popErrorScope((error: GPUError) => {
206✔
339
        this.reportError(new Error(`${this} command submission: ${error.message}`), this)();
×
340
        this.debug();
×
341
      });
342

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

405
  private _finalizeDefaultCommandEncoderForSubmit(): {
406
    submittedCommandEncoder: WebGPUCommandEncoder;
407
    commandBuffer: WebGPUCommandBuffer;
408
  } {
409
    const submittedCommandEncoder = this.commandEncoder;
201✔
410
    if (
201!
411
      submittedCommandEncoder.getTimeProfilingSlotCount() > 0 &&
201!
412
      submittedCommandEncoder.getTimeProfilingQuerySet() instanceof WebGPUQuerySet
413
    ) {
414
      const querySet = submittedCommandEncoder.getTimeProfilingQuerySet() as WebGPUQuerySet;
×
415
      querySet._encodeResolveToReadBuffer(submittedCommandEncoder, {
×
416
        firstQuery: 0,
417
        queryCount: submittedCommandEncoder.getTimeProfilingSlotCount()
418
      });
419
    }
420

421
    const commandBuffer = submittedCommandEncoder.finish();
201✔
422
    this.commandEncoder.destroy();
201✔
423
    this.commandEncoder = this.createCommandEncoder({
201✔
424
      id: submittedCommandEncoder.props.id,
425
      timeProfilingQuerySet: submittedCommandEncoder.getTimeProfilingQuerySet()
426
    });
427

428
    return {submittedCommandEncoder, commandBuffer};
201✔
429
  }
430

431
  // WebGPU specific
432

433
  pushErrorScope(scope: 'validation' | 'out-of-memory'): void {
434
    if (!this.props.debug) {
4,806!
435
      return;
×
436
    }
437
    const profiler = getWebGPUCpuHotspotProfiler(this);
4,806✔
438
    const startTime = profiler ? getTimestamp() : 0;
4,806✔
439
    this.handle.pushErrorScope(scope);
4,806✔
440
    if (profiler) {
4,806✔
441
      profiler.errorScopePushCount = (profiler.errorScopePushCount || 0) + 1;
149✔
442
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
149✔
443
    }
444
  }
445

446
  popErrorScope(handler: (error: GPUError) => void): Promise<void> {
447
    if (!this.props.debug) {
4,806!
NEW
448
      return Promise.resolve();
×
449
    }
450
    const profiler = getWebGPUCpuHotspotProfiler(this);
4,806✔
451
    const startTime = profiler ? getTimestamp() : 0;
4,806✔
452
    const errorScopePromise = this.handle
4,806✔
453
      .popErrorScope()
454
      .then((error: GPUError | null) => {
455
        if (error) {
4,378✔
456
          handler(error);
10✔
457
        }
458
      })
459
      .catch((error: unknown) => {
460
        if (this.shouldIgnoreDroppedInstanceError(error, 'popErrorScope')) {
427!
461
          return;
427✔
462
        }
463

464
        const errorMessage = error instanceof Error ? error.message : String(error);
×
465
        this.reportError(new Error(`${this} popErrorScope failed: ${errorMessage}`), this)();
427✔
466
        this.debug();
427✔
467
      });
468
    if (profiler) {
4,806✔
469
      profiler.errorScopePopCount = (profiler.errorScopePopCount || 0) + 1;
149✔
470
      profiler.errorScopeTimeMs = (profiler.errorScopeTimeMs || 0) + (getTimestamp() - startTime);
149✔
471
    }
472
    return errorScopePromise;
4,806✔
473
  }
474

475
  // PRIVATE METHODS
476

477
  protected _getInfo(): DeviceInfo {
478
    const [driver, driverVersion] = ((this.adapterInfo as any).driver || '').split(' Version ');
59✔
479

480
    // See https://developer.chrome.com/blog/new-in-webgpu-120#adapter_information_updates
481
    const vendor = this.adapterInfo.vendor || this.adapter.__brand || 'unknown';
59!
482
    const renderer = driver || '';
59✔
483
    const version = driverVersion || '';
59✔
484
    const fallback = Boolean(
59✔
485
      (this.adapterInfo as any).isFallbackAdapter ??
59!
486
        (this.adapter as any).isFallbackAdapter ??
487
        false
488
    );
489
    const softwareRenderer = /SwiftShader/i.test(
59✔
490
      `${vendor} ${renderer} ${this.adapterInfo.architecture || ''}`
59!
491
    );
492

493
    const gpuArchitecture = this.adapterInfo.architecture || 'unknown';
59!
494
    const gpu =
495
      identifyGPUVendor(vendor, renderer) ??
59✔
496
      (softwareRenderer || fallback ? 'software' : 'unknown');
118!
497
    const gpuBackend = (this.adapterInfo as any).backend || 'unknown';
59✔
498
    const gpuType =
59✔
499
      ((this.adapterInfo as any).type || '').split(' ')[0].toLowerCase() ||
118✔
500
      (softwareRenderer || fallback ? 'cpu' : 'unknown');
118!
501

502
    return {
59✔
503
      type: 'webgpu',
504
      vendor,
505
      renderer,
506
      version,
507
      gpu,
508
      gpuType,
509
      gpuBackend,
510
      gpuArchitecture,
511
      fallback,
512
      featureLevel: getWebGPUDeviceFeatureLevel(this.props.featureLevel),
513
      shadingLanguage: 'wgsl',
514
      shadingLanguageVersion: 100
515
    };
516
  }
517

518
  shouldIgnoreDroppedInstanceError(error: unknown, operation?: string): boolean {
519
    const errorMessage = error instanceof Error ? error.message : String(error);
437!
520
    return (
437✔
521
      errorMessage.includes('Instance dropped') &&
1,748!
522
      (!operation || errorMessage.includes(operation)) &&
523
      (this._isLost ||
524
        this.info.gpu === 'software' ||
525
        this.info.gpuType === 'cpu' ||
526
        Boolean(this.info.fallback))
527
    );
528
  }
529

530
  protected _getFeatures(): DeviceFeatures {
531
    // Initialize with actual WebGPU Features (note that unknown features may not be in DeviceFeature type)
532
    const features = new Set<DeviceFeature>(this.handle.features as Set<DeviceFeature>);
59✔
533
    // Fixups for pre-standard names: https://github.com/webgpu-native/webgpu-headers/issues/133
534
    // @ts-expect-error Chrome Canary v99
535
    if (features.has('depth-clamping')) {
59!
536
      // @ts-expect-error Chrome Canary v99
537
      features.delete('depth-clamping');
×
538
      features.add('depth-clip-control');
×
539
    }
540

541
    // Some subsets of WebGPU extensions correspond to WebGL extensions
542
    if (features.has('texture-compression-bc')) {
59✔
543
      features.add('texture-compression-bc5-webgl');
58✔
544
    }
545

546
    if (this.handle.features.has('chromium-experimental-norm16-texture-formats')) {
59!
547
      features.add('norm16-renderable-webgl');
×
548
    }
549

550
    if (this.handle.features.has('chromium-experimental-snorm16-texture-formats')) {
59!
551
      features.add('snorm16-renderable-webgl');
×
552
    }
553

554
    const WEBGPU_ALWAYS_FEATURES: DeviceFeature[] = [
59✔
555
      'compilation-status-async-webgl',
556
      'float32-renderable-webgl',
557
      'float16-renderable-webgl',
558
      'norm16-renderable-webgl',
559
      'texture-filterable-anisotropic-webgl',
560
      'shader-noperspective-interpolation-webgl'
561
    ];
562

563
    for (const feature of WEBGPU_ALWAYS_FEATURES) {
59✔
564
      features.add(feature);
354✔
565
    }
566

567
    if (
59!
568
      isHTMLInCanvasSupported() &&
59!
569
      typeof (
570
        this.handle.queue as GPUQueue & {
571
          copyElementImageToTexture?: unknown;
572
        }
573
      ).copyElementImageToTexture === 'function'
574
    ) {
575
      features.add('html-in-canvas');
×
576
    }
577

578
    return new DeviceFeatures(Array.from(features), this.props._disabledFeatures);
59✔
579
  }
580

581
  override _getDeviceSpecificTextureFormatCapabilities(
582
    capabilities: DeviceTextureFormatCapabilities
583
  ): DeviceTextureFormatCapabilities {
584
    const {format} = capabilities;
73✔
585
    if (format.includes('webgl')) {
73✔
586
      return {format, create: false, render: false, filter: false, blend: false, store: false};
11✔
587
    }
588
    return capabilities;
62✔
589
  }
590
}
591

592
function getWebGPUDeviceLimits(limits: GPUSupportedLimits): DeviceLimits {
593
  const stageSpecificLimits: Partial<Record<keyof DeviceLimits, number>> = {
59✔
594
    maxStorageBuffersInVertexStage:
595
      limits.maxStorageBuffersInVertexStage ?? limits.maxStorageBuffersPerShaderStage,
59!
596
    maxStorageBuffersInFragmentStage:
597
      limits.maxStorageBuffersInFragmentStage ?? limits.maxStorageBuffersPerShaderStage,
59!
598
    maxStorageTexturesInVertexStage:
599
      limits.maxStorageTexturesInVertexStage ?? limits.maxStorageTexturesPerShaderStage,
59!
600
    maxStorageTexturesInFragmentStage:
601
      limits.maxStorageTexturesInFragmentStage ?? limits.maxStorageTexturesPerShaderStage
59!
602
  };
603

604
  return new Proxy(limits, {
59✔
605
    get(target, property) {
606
      return typeof property === 'string' && property in stageSpecificLimits
422✔
607
        ? stageSpecificLimits[property as keyof DeviceLimits]
608
        : Reflect.get(target, property, target);
609
    }
610
  }) as DeviceLimits;
611
}
612

613
function getWebGPUDeviceFeatureLevel(
614
  featureLevel: DeviceProps['featureLevel']
615
): NonNullable<DeviceInfo['featureLevel']> {
616
  return featureLevel === 'best-available' ? 'core' : featureLevel || 'core';
59!
617
}
618

619
function identifyGPUVendor(
620
  vendor: string,
621
  renderer: string
622
): 'nvidia' | 'intel' | 'apple' | 'amd' | null {
623
  if (/NVIDIA/i.exec(vendor) || /NVIDIA/i.exec(renderer)) {
59!
624
    return 'nvidia';
×
625
  }
626
  if (/INTEL/i.exec(vendor) || /INTEL/i.exec(renderer)) {
59!
627
    return 'intel';
×
628
  }
629
  if (/Apple/i.exec(vendor) || /Apple/i.exec(renderer)) {
59!
630
    return 'apple';
×
631
  }
632
  if (
59!
633
    /AMD/i.exec(vendor) ||
236✔
634
    /AMD/i.exec(renderer) ||
635
    /ATI/i.exec(vendor) ||
636
    /ATI/i.exec(renderer)
637
  ) {
638
    return 'amd';
×
639
  }
640
  return null;
59✔
641
}
642

643
function scheduleMicrotask(callback: () => void): void {
644
  if (globalThis.queueMicrotask) {
201!
645
    globalThis.queueMicrotask(callback);
201✔
646
    return;
201✔
647
  }
648
  Promise.resolve()
×
649
    .then(callback)
650
    .catch(() => {});
651
}
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