• 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

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

5
import {StatsManager, lumaStats} from '../utils/stats-manager';
6
import {log} from '../utils/log';
7
import {uid} from '../utils/uid';
8
import type {VertexFormat, VertexFormatInfo} from '../shadertypes/vertex-types/vertex-formats';
9
import type {
10
  TextureFormat,
11
  TextureFormatInfo,
12
  CompressedTextureFormat
13
} from '../shadertypes/texture-types/texture-formats';
14
import type {CanvasContext, CanvasContextProps} from './canvas-context';
15
import type {PresentationContext, PresentationContextProps} from './presentation-context';
16
import type {BufferProps} from './resources/buffer';
17
import {Buffer} from './resources/buffer';
18
import type {RenderPipeline, RenderPipelineProps} from './resources/render-pipeline';
19
import type {SharedRenderPipeline} from './resources/shared-render-pipeline';
20
import type {ComputePipeline, ComputePipelineProps} from './resources/compute-pipeline';
21
import type {Sampler, SamplerProps} from './resources/sampler';
22
import type {Shader, ShaderProps} from './resources/shader';
23
import type {Texture, TextureProps} from './resources/texture';
24
import type {ExternalTexture, ExternalTextureProps} from './resources/external-texture';
25
import type {Framebuffer, FramebufferProps} from './resources/framebuffer';
26
import type {RenderPass, RenderPassProps} from './resources/render-pass';
27
import type {ComputePass, ComputePassProps} from './resources/compute-pass';
28
import type {CommandEncoder, CommandEncoderProps} from './resources/command-encoder';
29
import type {CommandBuffer} from './resources/command-buffer';
30
import type {VertexArray, VertexArrayProps} from './resources/vertex-array';
31
import type {TransformFeedback, TransformFeedbackProps} from './resources/transform-feedback';
32
import type {QuerySet, QuerySetProps} from './resources/query-set';
33
import type {Fence} from './resources/fence';
34

35
import {vertexFormatDecoder} from '../shadertypes/vertex-types/vertex-format-decoder';
36
import {textureFormatDecoder} from '../shadertypes/texture-types/texture-format-decoder';
37
import type {ExternalImage} from '../shadertypes/image-types/image-types';
38
import {isExternalImage, getExternalImageSize} from '../shadertypes/image-types/image-types';
39
import {getTextureFormatTable} from '../shadertypes/texture-types/texture-format-table';
40

41
/**
42
 * Identifies the GPU vendor and driver.
43
 * @note Chrome WebGPU does not provide much information, though more can be enabled with
44
 * @see https://developer.chrome.com/blog/new-in-webgpu-120#adapter_information_updates
45
 * chrome://flags/#enable-webgpu-developer-features
46
 */
47
export type DeviceInfo = {
48
  /** Type of device */
49
  type: 'webgl' | 'webgpu' | 'null' | 'unknown';
50
  /** Vendor (name of GPU vendor, Apple, nVidia etc */
51
  vendor: string;
52
  /** Renderer (usually driver name) */
53
  renderer: string;
54
  /** version of driver */
55
  version: string;
56
  /** family of GPU */
57
  gpu: 'nvidia' | 'amd' | 'intel' | 'apple' | 'software' | 'unknown';
58
  /** Type of GPU () */
59
  gpuType: 'discrete' | 'integrated' | 'cpu' | 'unknown';
60
  /** GPU architecture */
61
  gpuArchitecture?: string; // 'common-3' on Apple
62
  /** GPU driver backend. Can sometimes be sniffed */
63
  gpuBackend?: 'opengl' | 'opengles' | 'metal' | 'd3d11' | 'd3d12' | 'vulkan' | 'unknown';
64
  /** If this is a fallback adapter */
65
  fallback?: boolean;
66
  /** Shader language supported by device.createShader() */
67
  shadingLanguage: 'wgsl' | 'glsl';
68
  /** Highest supported shader language version: GLSL 3.00 = 300, WGSL 1.00 = 100 */
69
  shadingLanguageVersion: number;
70
};
71

72
/** Limits for a device (max supported sizes of resources, max number of bindings etc) */
73
export abstract class DeviceLimits {
74
  /** max number of TextureDimension1D */
75
  abstract maxTextureDimension1D: number;
76
  /** max number of TextureDimension2D */
77
  abstract maxTextureDimension2D: number;
78
  /** max number of TextureDimension3D */
79
  abstract maxTextureDimension3D: number;
80
  /** max number of TextureArrayLayers */
81
  abstract maxTextureArrayLayers: number;
82
  /** max number of BindGroups */
83
  abstract maxBindGroups: number;
84
  /** max number of DynamicUniformBuffers per PipelineLayout */
85
  abstract maxDynamicUniformBuffersPerPipelineLayout: number;
86
  /** max number of DynamicStorageBuffers per PipelineLayout */
87
  abstract maxDynamicStorageBuffersPerPipelineLayout: number;
88
  /** max number of SampledTextures per ShaderStage */
89
  abstract maxSampledTexturesPerShaderStage: number;
90
  /** max number of Samplers per ShaderStage */
91
  abstract maxSamplersPerShaderStage: number;
92
  /** max number of StorageBuffers per ShaderStage */
93
  abstract maxStorageBuffersPerShaderStage: number;
94
  /** max number of StorageTextures per ShaderStage */
95
  abstract maxStorageTexturesPerShaderStage: number;
96
  /** max number of UniformBuffers per ShaderStage */
97
  abstract maxUniformBuffersPerShaderStage: number;
98
  /** max number of UniformBufferBindingSize */
99
  abstract maxUniformBufferBindingSize: number;
100
  /** max number of StorageBufferBindingSize */
101
  abstract maxStorageBufferBindingSize: number;
102
  /** min UniformBufferOffsetAlignment */
103
  abstract minUniformBufferOffsetAlignment: number;
104
  /** min StorageBufferOffsetAlignment */
105
  abstract minStorageBufferOffsetAlignment: number;
106
  /** max number of VertexBuffers */
107
  abstract maxVertexBuffers: number;
108
  /** max number of VertexAttributes */
109
  abstract maxVertexAttributes: number;
110
  /** max number of VertexBufferArrayStride */
111
  abstract maxVertexBufferArrayStride: number;
112
  /** max number of InterStageShaderComponents */
113
  abstract maxInterStageShaderVariables: number;
114
  /** max number of ComputeWorkgroupStorageSize */
115
  abstract maxComputeWorkgroupStorageSize: number;
116
  /** max number of ComputeInvocations per Workgroup */
117
  abstract maxComputeInvocationsPerWorkgroup: number;
118
  /** max ComputeWorkgroupSizeX */
119
  abstract maxComputeWorkgroupSizeX: number;
120
  /** max ComputeWorkgroupSizeY */
121
  abstract maxComputeWorkgroupSizeY: number;
122
  /** max ComputeWorkgroupSizeZ */
123
  abstract maxComputeWorkgroupSizeZ: number;
124
  /** max ComputeWorkgroupsPerDimension */
125
  abstract maxComputeWorkgroupsPerDimension: number;
126
}
127

128
function formatErrorLogArguments(context: unknown, args: unknown[]): unknown[] {
129
  const formattedContext = formatErrorLogValue(context);
7✔
130
  const formattedArgs = args.map(formatErrorLogValue).filter(arg => arg !== undefined);
7✔
131
  return [formattedContext, ...formattedArgs].filter(arg => arg !== undefined);
9✔
132
}
133

134
function formatErrorLogValue(value: unknown): unknown {
135
  if (value === undefined) {
10!
NEW
136
    return undefined;
×
137
  }
138
  if (
10✔
139
    value === null ||
38✔
140
    typeof value === 'string' ||
141
    typeof value === 'number' ||
142
    typeof value === 'boolean'
143
  ) {
144
    return value;
1✔
145
  }
146
  if (value instanceof Error) {
9!
NEW
147
    return value.message;
×
148
  }
149
  if (Array.isArray(value)) {
9✔
150
    return value.map(formatErrorLogValue);
1✔
151
  }
152
  if (typeof value === 'object') {
8!
153
    if (hasCustomToString(value)) {
8✔
154
      const stringValue = String(value);
7✔
155
      if (stringValue !== '[object Object]') {
7!
156
        return stringValue;
7✔
157
      }
158
    }
159

160
    if (looksLikeGPUCompilationMessage(value)) {
1!
161
      return formatGPUCompilationMessage(value);
1✔
162
    }
163

NEW
164
    return value.constructor?.name || 'Object';
×
165
  }
166

NEW
167
  return String(value);
×
168
}
169

170
function hasCustomToString(value: object): boolean {
171
  return (
8✔
172
    'toString' in value &&
24✔
173
    typeof value.toString === 'function' &&
174
    value.toString !== Object.prototype.toString
175
  );
176
}
177

178
function looksLikeGPUCompilationMessage(value: object): value is {
179
  message?: unknown;
180
  type?: unknown;
181
  lineNum?: unknown;
182
  linePos?: unknown;
183
} {
184
  return 'message' in value && 'type' in value;
1✔
185
}
186

187
function formatGPUCompilationMessage(value: {
188
  message?: unknown;
189
  type?: unknown;
190
  lineNum?: unknown;
191
  linePos?: unknown;
192
}): string {
193
  const type = typeof value.type === 'string' ? value.type : 'message';
1!
194
  const message = typeof value.message === 'string' ? value.message : '';
1!
195
  const lineNum = typeof value.lineNum === 'number' ? value.lineNum : null;
1!
196
  const linePos = typeof value.linePos === 'number' ? value.linePos : null;
1!
197
  const location =
198
    lineNum !== null && linePos !== null
1!
199
      ? ` @ ${lineNum}:${linePos}`
200
      : lineNum !== null
×
201
        ? ` @ ${lineNum}`
202
        : '';
203
  return `${type}${location}: ${message}`.trim();
1✔
204
}
205

206
/** Set-like class for features (lets apps check for WebGL / WebGPU extensions) */
207
export class DeviceFeatures {
208
  protected features: Set<DeviceFeature>;
209
  protected disabledFeatures?: Partial<Record<DeviceFeature, boolean>>;
210

211
  constructor(
212
    features: DeviceFeature[] = [],
147✔
213
    disabledFeatures: Partial<Record<DeviceFeature, boolean>>
214
  ) {
215
    this.features = new Set<DeviceFeature>(features);
147✔
216
    this.disabledFeatures = disabledFeatures || {};
147!
217
  }
218

219
  *[Symbol.iterator](): IterableIterator<DeviceFeature> {
220
    yield* this.features;
×
221
  }
222

223
  has(feature: DeviceFeature): boolean {
224
    return !this.disabledFeatures?.[feature] && this.features.has(feature);
91✔
225
  }
226
}
227

228
/** Device feature names */
229
export type DeviceFeature =
230
  | WebGPUDeviceFeature
231
  | WebGLDeviceFeature
232
  | WebGLCompressedTextureFeatures;
233
// | ChromeExperimentalFeatures
234

235
/** Chrome-specific extensions. Expected to eventually become standard features. */
236
// export type ChromeExperimentalFeatures = ;
237

238
export type WebGPUDeviceFeature =
239
  | 'depth-clip-control'
240
  | 'depth32float-stencil8'
241
  | 'texture-compression-bc'
242
  | 'texture-compression-bc-sliced-3d'
243
  | 'texture-compression-etc2'
244
  | 'texture-compression-astc'
245
  | 'texture-compression-astc-sliced-3d'
246
  | 'timestamp-query'
247
  | 'indirect-first-instance'
248
  | 'shader-f16'
249
  | 'rg11b10ufloat-renderable' // Is the rg11b10ufloat texture format renderable?
250
  | 'bgra8unorm-storage' // Can the bgra8unorm texture format be used in storage buffers?
251
  | 'float32-filterable' // Is the float32 format filterable?
252
  | 'float32-blendable' // Is the float32 format blendable?
253
  | 'clip-distances'
254
  | 'dual-source-blending'
255
  | 'subgroups';
256
// | 'depth-clamping' // removed from the WebGPU spec...
257
// | 'pipeline-statistics-query' // removed from the WebGPU spec...
258

259
export type WebGLDeviceFeature =
260
  // webgl extension features
261
  | 'compilation-status-async-webgl' // Non-blocking shader compile/link status query available
262
  | 'provoking-vertex-webgl' // parameters.provokingVertex
263
  | 'polygon-mode-webgl' // parameters.polygonMode and parameters.polygonOffsetLine
264

265
  // GLSL extension features
266
  | 'shader-noperspective-interpolation-webgl' // Vertex outputs & fragment inputs can have a `noperspective` interpolation qualifier.
267
  | 'shader-conservative-depth-webgl' // GLSL `gl_FragDepth` qualifiers `depth_unchanged` etc can enable early depth test
268
  | 'shader-clip-cull-distance-webgl' // Makes gl_ClipDistance and gl_CullDistance available in shaders
269

270
  // texture rendering
271
  | 'float32-renderable-webgl'
272
  | 'float16-renderable-webgl'
273
  | 'rgb9e5ufloat-renderable-webgl'
274
  | 'snorm8-renderable-webgl'
275
  | 'norm16-webgl'
276
  | 'norm16-renderable-webgl'
277
  | 'snorm16-renderable-webgl'
278

279
  // texture filtering
280
  | 'float16-filterable-webgl'
281
  | 'texture-filterable-anisotropic-webgl'
282

283
  // texture storage bindings
284
  | 'bgra8unorm-storage'
285

286
  // texture blending
287
  | 'texture-blend-float-webgl';
288

289
type WebGLCompressedTextureFeatures =
290
  | 'texture-compression-bc5-webgl'
291
  | 'texture-compression-bc7-webgl'
292
  | 'texture-compression-etc1-webgl'
293
  | 'texture-compression-pvrtc-webgl'
294
  | 'texture-compression-atc-webgl';
295

296
/** Texture format capabilities that have been checked against a specific device */
297
export type DeviceTextureFormatCapabilities = {
298
  format: TextureFormat;
299
  /** Can the format be created and sampled?*/
300
  create: boolean;
301
  /** Is the format renderable. */
302
  render: boolean;
303
  /** Is the format filterable. */
304
  filter: boolean;
305
  /** Is the format blendable. */
306
  blend: boolean;
307
  /** Is the format storeable. */
308
  store: boolean;
309
};
310

311
/** Device properties */
312
export type DeviceProps = {
313
  /** string id for debugging. Stored on the object, used in logging and set on underlying GPU objects when feasible. */
314
  id?: string;
315
  /** Properties for creating a default canvas context */
316
  createCanvasContext?: CanvasContextProps | true;
317
  /** Control which type of GPU is preferred on systems with both integrated and discrete GPU. Defaults to "high-performance" / discrete GPU. */
318
  powerPreference?: 'default' | 'high-performance' | 'low-power';
319
  /** Hints that device creation should fail if no hardware GPU is available (if the system performance is "low"). */
320
  failIfMajorPerformanceCaveat?: boolean;
321

322
  /** WebGL specific: Properties passed through to WebGL2RenderingContext creation: `canvas.getContext('webgl2', props.webgl)` */
323
  webgl?: WebGLContextProps;
324

325
  // CALLBACKS
326

327
  /** Error handler. If it returns a probe logger style function, it will be called at the site of the error to optimize console error links. */
328
  onError?: (error: Error, context?: unknown) => unknown;
329
  /** Called when the size of a CanvasContext's canvas changes */
330
  onResize?: (
331
    ctx: CanvasContext | PresentationContext,
332
    info: {oldPixelSize: [number, number]}
333
  ) => unknown;
334
  /** Called when the absolute position of a CanvasContext's canvas changes. Must set `CanvasContextProps.trackPosition: true` */
335
  onPositionChange?: (
336
    ctx: CanvasContext | PresentationContext,
337
    info: {oldPosition: [number, number]}
338
  ) => unknown;
339
  /** Called when the visibility of a CanvasContext's canvas changes */
340
  onVisibilityChange?: (ctx: CanvasContext | PresentationContext) => unknown;
341
  /** Called when the device pixel ratio of a CanvasContext's canvas changes */
342
  onDevicePixelRatioChange?: (
343
    ctx: CanvasContext | PresentationContext,
344
    info: {oldRatio: number}
345
  ) => unknown;
346

347
  // DEBUG SETTINGS
348

349
  /** Turn on implementation defined checks that slow down execution but help break where errors occur */
350
  debug?: boolean;
351
  /** Enable GPU timestamp collection without enabling all debug validation paths. */
352
  debugGPUTime?: boolean;
353
  /** Show shader source in browser? The default is `'error'`, meaning that logs are shown when shader compilation has errors */
354
  debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
355
  /** Renders a small version of updated Framebuffers into the primary canvas context. Can be set in console luma.log.set('debug-framebuffers', true) */
356
  debugFramebuffers?: boolean;
357
  /** Traces resource caching, reuse, and destroys in the PipelineFactory */
358
  debugFactories?: boolean;
359
  /** WebGL specific - Trace WebGL calls (instruments WebGL2RenderingContext at the expense of performance). Can be set in console luma.log.set('debug-webgl', true)  */
360
  debugWebGL?: boolean;
361
  /** WebGL specific - Initialize the SpectorJS WebGL debugger. Can be set in console luma.log.set('debug-spectorjs', true)  */
362
  debugSpectorJS?: boolean;
363
  /** WebGL specific - SpectorJS URL. Override if CDN is down or different SpectorJS version is desired. */
364
  debugSpectorJSUrl?: string;
365

366
  // EXPERIMENTAL SETTINGS - subject to change
367

368
  /** adapter.create() returns the existing Device if the provided canvas' WebGL context is already associated with a Device.  */
369
  _reuseDevices?: boolean;
370
  /** WebGPU specific - Request a Device with the highest limits supported by platform. On WebGPU devices can be created with minimal limits. */
371
  _requestMaxLimits?: boolean;
372
  /** Disable specific features */
373
  _disabledFeatures?: Partial<Record<DeviceFeature, boolean>>;
374
  /** WebGL specific - Initialize all features on startup */
375
  _initializeFeatures?: boolean;
376
  /** Enable shader caching (via ShaderFactory) */
377
  _cacheShaders?: boolean;
378
  /**
379
   * Destroy cached shaders when they become unused.
380
   * Defaults to `false` so repeated create/destroy cycles can still reuse cached shaders.
381
   * Enable this if the application creates very large numbers of distinct shaders and needs cache eviction.
382
   */
383
  _destroyShaders?: boolean;
384
  /** Enable pipeline caching (via PipelineFactory) */
385
  _cachePipelines?: boolean;
386
  /** Enable sharing of backend render-pipeline implementations when caching is enabled. Currently used by WebGL. */
387
  _sharePipelines?: boolean;
388
  /**
389
   * Destroy cached pipelines when they become unused.
390
   * Defaults to `false` so repeated create/destroy cycles can still reuse cached pipelines.
391
   * Enable this if the application creates very large numbers of distinct pipelines and needs cache eviction.
392
   */
393
  _destroyPipelines?: boolean;
394

395
  /** @deprecated Internal, Do not use directly! Use `luma.attachDevice()` to attach to pre-created contexts/devices. */
396
  _handle?: unknown; // WebGL2RenderingContext | GPUDevice | null;
397
};
398

399
/** WebGL independent copy of WebGLContextAttributes */
400
type WebGLContextProps = {
401
  /** indicates if the canvas contains an alpha buffer. */
402
  alpha?: boolean;
403
  /** hints the user agent to reduce the latency by desynchronizing the canvas paint cycle from the event loop */
404
  desynchronized?: boolean;
405
  /** indicates whether or not to perform anti-aliasing. */
406
  antialias?: boolean;
407
  /** indicates that the render target has a stencil buffer of at least `8` bits. */
408
  stencil?: boolean;
409
  /** indicates that the drawing buffer has a depth buffer of at least 16 bits. */
410
  depth?: boolean;
411
  /** indicates if a context will be created if the system performance is low or if no hardware GPU is available. */
412
  failIfMajorPerformanceCaveat?: boolean;
413
  /** Selects GPU */
414
  powerPreference?: 'default' | 'high-performance' | 'low-power';
415
  /** page compositor will assume the drawing buffer contains colors with pre-multiplied alpha. */
416
  premultipliedAlpha?: boolean;
417
  /** buffers will not be cleared and will preserve their values until cleared or overwritten by the author. */
418
  preserveDrawingBuffer?: boolean;
419
};
420

421
/**
422
 * Create and attach devices for a specific backend. Currently static methods on each device
423
 */
424
export interface DeviceFactory {
425
  // new (props: DeviceProps): Device; Constructor isn't used
426
  type: string;
427
  isSupported(): boolean;
428
  create(props: DeviceProps): Promise<Device>;
429
  attach?(handle: unknown): Device;
430
}
431

432
/**
433
 * WebGPU Device/WebGL context abstraction
434
 */
435
export abstract class Device {
436
  static defaultProps: Required<DeviceProps> = {
113✔
437
    id: null!,
438
    powerPreference: 'high-performance',
439
    failIfMajorPerformanceCaveat: false,
440
    createCanvasContext: undefined!,
441
    // WebGL specific
442
    webgl: {},
443

444
    // Callbacks
445
    // eslint-disable-next-line handle-callback-err
446
    onError: (error: Error, context: unknown) => {},
447
    onResize: (context: CanvasContext, info: {oldPixelSize: [number, number]}) => {
448
      const [width, height] = context.getDevicePixelSize();
150✔
449
      log.log(1, `${context} resized => ${width}x${height}px`)();
150✔
450
    },
451
    onPositionChange: (context: CanvasContext, info: {oldPosition: [number, number]}) => {
452
      const [left, top] = context.getPosition();
142✔
453
      log.log(1, `${context} repositioned => ${left},${top}`)();
142✔
454
    },
455
    onVisibilityChange: (context: CanvasContext) =>
456
      log.log(1, `${context} Visibility changed ${context.isVisible}`)(),
7✔
457
    onDevicePixelRatioChange: (context: CanvasContext, info: {oldRatio: number}) =>
458
      log.log(1, `${context} DPR changed ${info.oldRatio} => ${context.devicePixelRatio}`)(),
150✔
459

460
    // Debug flags
461
    debug: getDefaultDebugValue(),
462
    debugGPUTime: false,
463
    debugShaders: log.get('debug-shaders') || undefined!,
226✔
464
    debugFramebuffers: Boolean(log.get('debug-framebuffers')),
465
    debugFactories: Boolean(log.get('debug-factories')),
466
    debugWebGL: Boolean(log.get('debug-webgl')),
467
    debugSpectorJS: undefined!, // Note: log setting is queried by the spector.js code
468
    debugSpectorJSUrl: undefined!,
469

470
    // Experimental
471
    _reuseDevices: false,
472
    _requestMaxLimits: true,
473
    _cacheShaders: true,
474
    _destroyShaders: false,
475
    _cachePipelines: true,
476
    _sharePipelines: true,
477
    _destroyPipelines: false,
478
    // TODO - Change these after confirming things work as expected
479
    _initializeFeatures: true,
480
    _disabledFeatures: {
481
      'compilation-status-async-webgl': true
482
    },
483

484
    // INTERNAL
485
    _handle: undefined!
486
  };
487

488
  get [Symbol.toStringTag](): string {
489
    return 'Device';
×
490
  }
491

492
  toString(): string {
493
    return `Device(${this.id})`;
×
494
  }
495

496
  /** id of this device, primarily for debugging */
497
  readonly id: string;
498
  /** type of this device */
499
  abstract readonly type: 'webgl' | 'webgpu' | 'null' | 'unknown';
500
  abstract readonly handle: unknown;
501
  abstract commandEncoder: CommandEncoder;
502

503
  /** A copy of the device props  */
504
  readonly props: Required<DeviceProps>;
505
  /** Available for the application to store data on the device */
506
  userData: {[key: string]: unknown} = {};
147✔
507
  /** stats */
508
  readonly statsManager: StatsManager = lumaStats;
147✔
509
  /** An abstract timestamp used for change tracking */
510
  timestamp: number = 0;
147✔
511

512
  /** True if this device has been reused during device creation (app has multiple references) */
513
  _reused: boolean = false;
147✔
514
  /** Used by other luma.gl modules to store data on the device */
515
  private _moduleData: Record<string, Record<string, unknown>> = {};
147✔
516

517
  // Capabilities
518

519
  /** Information about the device (vendor, versions etc) */
520
  abstract info: DeviceInfo;
521
  /** Optional capability discovery */
522
  abstract features: DeviceFeatures;
523
  /** WebGPU style device limits */
524
  abstract get limits(): DeviceLimits;
525

526
  // Texture helpers
527

528
  /** Optimal TextureFormat for displaying 8-bit depth, standard dynamic range content on this system. */
529
  abstract preferredColorFormat: 'rgba8unorm' | 'bgra8unorm';
530
  /** Default depth format used on this system */
531
  abstract preferredDepthFormat: 'depth16' | 'depth24plus' | 'depth32float';
532

533
  protected _textureCaps: Partial<Record<TextureFormat, DeviceTextureFormatCapabilities>> = {};
147✔
534
  /** Internal timestamp query set used when GPU timing collection is enabled for this device. */
535
  protected _debugGPUTimeQuery: QuerySet | null = null;
147✔
536

537
  constructor(props: DeviceProps) {
538
    this.props = {...Device.defaultProps, ...props};
147✔
539
    this.id = this.props.id || uid(this[Symbol.toStringTag].toLowerCase());
147!
540
  }
541

542
  abstract destroy(): void;
543

544
  // TODO - just expose the shadertypes decoders?
545

546
  getVertexFormatInfo(format: VertexFormat): VertexFormatInfo {
547
    return vertexFormatDecoder.getVertexFormatInfo(format);
×
548
  }
549

550
  isVertexFormatSupported(format: VertexFormat): boolean {
551
    return true;
×
552
  }
553

554
  /** Returns information about a texture format, such as data type, channels, bits per channel, compression etc */
555
  getTextureFormatInfo(format: TextureFormat): TextureFormatInfo {
556
    return textureFormatDecoder.getInfo(format);
527✔
557
  }
558

559
  /** Determines what operations are supported on a texture format on this particular device (checks against supported device features) */
560
  getTextureFormatCapabilities(format: TextureFormat): DeviceTextureFormatCapabilities {
561
    let textureCaps = this._textureCaps[format];
319✔
562
    if (!textureCaps) {
319✔
563
      const capabilities = this._getDeviceTextureFormatCapabilities(format);
176✔
564
      textureCaps = this._getDeviceSpecificTextureFormatCapabilities(capabilities);
176✔
565
      this._textureCaps[format] = textureCaps;
176✔
566
    }
567
    return textureCaps;
319✔
568
  }
569

570
  /** Calculates the number of mip levels for a texture of width, height and in case of 3d textures only, depth */
571
  getMipLevelCount(width: number, height: number, depth3d: number = 1): number {
83✔
572
    const maxSize = Math.max(width, height, depth3d);
83✔
573
    return 1 + Math.floor(Math.log2(maxSize));
83✔
574
  }
575

576
  /** Check if data is an external image */
577
  isExternalImage(data: unknown): data is ExternalImage {
578
    return isExternalImage(data);
269✔
579
  }
580

581
  /** Get the size of an external image */
582
  getExternalImageSize(data: ExternalImage): {width: number; height: number} {
583
    return getExternalImageSize(data);
143✔
584
  }
585

586
  /** Check if device supports a specific texture format (creation and `nearest` sampling) */
587
  isTextureFormatSupported(format: TextureFormat): boolean {
588
    return this.getTextureFormatCapabilities(format).create;
245✔
589
  }
590

591
  /** Check if linear filtering (sampler interpolation) is supported for a specific texture format */
592
  isTextureFormatFilterable(format: TextureFormat): boolean {
593
    return this.getTextureFormatCapabilities(format).filter;
13✔
594
  }
595

596
  /** Check if device supports rendering to a framebuffer color attachment of a specific texture format */
597
  isTextureFormatRenderable(format: TextureFormat): boolean {
598
    return this.getTextureFormatCapabilities(format).render;
39✔
599
  }
600

601
  /** Check if a specific texture format is GPU compressed */
602
  isTextureFormatCompressed(format: TextureFormat): boolean {
603
    return textureFormatDecoder.isCompressed(format);
433✔
604
  }
605

606
  /** Returns the compressed texture formats that can be created and sampled on this device */
607
  getSupportedCompressedTextureFormats(): CompressedTextureFormat[] {
608
    const supportedFormats: CompressedTextureFormat[] = [];
2✔
609

610
    for (const format of Object.keys(getTextureFormatTable()) as TextureFormat[]) {
2✔
611
      if (this.isTextureFormatCompressed(format) && this.isTextureFormatSupported(format)) {
238✔
612
        supportedFormats.push(format as CompressedTextureFormat);
107✔
613
      }
614
    }
615

616
    return supportedFormats;
2✔
617
  }
618

619
  // DEBUG METHODS
620

621
  pushDebugGroup(groupLabel: string): void {
622
    this.commandEncoder.pushDebugGroup(groupLabel);
×
623
  }
624

625
  popDebugGroup(): void {
626
    this.commandEncoder?.popDebugGroup();
×
627
  }
628

629
  insertDebugMarker(markerLabel: string): void {
630
    this.commandEncoder?.insertDebugMarker(markerLabel);
×
631
  }
632

633
  // Device loss
634

635
  /** `true` if device is already lost */
636
  abstract get isLost(): boolean;
637

638
  /** Promise that resolves when device is lost */
639
  abstract readonly lost: Promise<{reason: 'destroyed'; message: string}>;
640

641
  /**
642
   * Trigger device loss.
643
   * @returns `true` if context loss could actually be triggered.
644
   * @note primarily intended for testing how application reacts to device loss
645
   */
646
  loseDevice(): boolean {
647
    return false;
×
648
  }
649

650
  /** A monotonic counter for tracking buffer and texture updates */
651
  incrementTimestamp(): number {
652
    return this.timestamp++;
851✔
653
  }
654

655
  /**
656
   * Reports Device errors in a way that optimizes for developer experience / debugging.
657
   * - Logs so that the console error links directly to the source code that generated the error.
658
   * - Includes the object that reported the error in the log message, even if the error is asynchronous.
659
   *
660
   * Conventions when calling reportError():
661
   * - Always call the returned function - to ensure error is logged, at the error site
662
   * - Follow with a call to device.debug() - to ensure that the debugger breaks at the error site
663
   *
664
   * @param error - the error to report. If needed, just create a new Error object with the appropriate message.
665
   * @param context - pass `this` as context, otherwise it may not be available in the debugger for async errors.
666
   * @returns the logger function returned by device.props.onError() so that it can be called from the error site.
667
   *
668
   * @example
669
   *   device.reportError(new Error(...), this)();
670
   *   device.debug();
671
   */
672
  reportError(error: Error, context: unknown, ...args: unknown[]): () => unknown {
673
    // Call the error handler
674
    const isHandled = this.props.onError(error, context);
7✔
675
    if (!isHandled) {
7!
676
      const logArguments = formatErrorLogArguments(context, args);
7✔
677
      // Note: Returns a function that must be called: `device.reportError(...)()`
678
      return log.error(
7✔
679
        this.type === 'webgl' ? '%cWebGL' : '%cWebGPU',
7!
680
        'color: white; background: red; padding: 2px 6px; border-radius: 3px;',
681
        error.message,
682
        ...logArguments
683
      );
684
    }
685
    return () => {};
×
686
  }
687

688
  /** Break in the debugger - if device.props.debug is true */
689
  debug(): void {
690
    if (this.props.debug) {
7!
691
      // @ts-ignore
692
      debugger; // eslint-disable-line
7✔
693
    } else {
694
      // TODO(ibgreen): Does not appear to be printed in the console
695
      const message = `\
×
696
'Type luma.log.set({debug: true}) in console to enable debug breakpoints',
697
or create a device with the 'debug: true' prop.`;
698
      log.once(0, message)();
×
699
    }
700
  }
701

702
  // Canvas context
703

704
  /** Default / primary canvas context. Can be null as WebGPU devices can be created without a CanvasContext */
705
  abstract canvasContext: CanvasContext | null;
706

707
  /** Returns the default / primary canvas context. Throws an error if no canvas context is available (a WebGPU compute device) */
708
  getDefaultCanvasContext(): CanvasContext {
709
    if (!this.canvasContext) {
207✔
710
      throw new Error('Device has no default CanvasContext. See props.createCanvasContext');
1✔
711
    }
712
    return this.canvasContext;
206✔
713
  }
714

715
  /** Creates a new CanvasContext (WebGPU only) */
716
  abstract createCanvasContext(props?: CanvasContextProps): CanvasContext;
717

718
  /** Creates a presentation context for a destination canvas. WebGL requires the default canvas context to use an OffscreenCanvas. */
719
  abstract createPresentationContext(props?: PresentationContextProps): PresentationContext;
720

721
  /** Call after rendering a frame (necessary e.g. on WebGL OffscreenCanvas) */
722
  abstract submit(commandBuffer?: CommandBuffer): void;
723

724
  // Resource creation
725

726
  /** Create a buffer */
727
  abstract createBuffer(props: BufferProps | ArrayBuffer | ArrayBufferView): Buffer;
728

729
  /** Create a texture */
730
  abstract createTexture(props: TextureProps): Texture;
731

732
  /** Create a temporary texture view of a video source */
733
  abstract createExternalTexture(props: ExternalTextureProps): ExternalTexture;
734

735
  /** Create a sampler */
736
  abstract createSampler(props: SamplerProps): Sampler;
737

738
  /** Create a Framebuffer. Must have at least one attachment. */
739
  abstract createFramebuffer(props: FramebufferProps): Framebuffer;
740

741
  /** Create a shader */
742
  abstract createShader(props: ShaderProps): Shader;
743

744
  /** Create a render pipeline (aka program) */
745
  abstract createRenderPipeline(props: RenderPipelineProps): RenderPipeline;
746

747
  /** Create a compute pipeline (aka program). WebGPU only. */
748
  abstract createComputePipeline(props: ComputePipelineProps): ComputePipeline;
749

750
  /** Create a vertex array */
751
  abstract createVertexArray(props: VertexArrayProps): VertexArray;
752

753
  abstract createCommandEncoder(props?: CommandEncoderProps): CommandEncoder;
754

755
  /** Create a transform feedback (immutable set of output buffer bindings). WebGL only. */
756
  abstract createTransformFeedback(props: TransformFeedbackProps): TransformFeedback;
757

758
  abstract createQuerySet(props: QuerySetProps): QuerySet;
759

760
  /** Create a fence sync object */
761
  createFence(): Fence {
762
    throw new Error('createFence() not implemented');
×
763
  }
764

765
  /** Create a RenderPass using the default CommandEncoder */
766
  beginRenderPass(props?: RenderPassProps): RenderPass {
767
    return this.commandEncoder.beginRenderPass(props);
129✔
768
  }
769

770
  /** Create a ComputePass using the default CommandEncoder*/
771
  beginComputePass(props?: ComputePassProps): ComputePass {
772
    return this.commandEncoder.beginComputePass(props);
6✔
773
  }
774

775
  /**
776
   * Generate mipmaps for a WebGPU texture.
777
   * WebGPU textures must be created up front with the required mip count, usage flags, and a format that supports the chosen generation path.
778
   * WebGL uses `Texture.generateMipmapsWebGL()` directly because the backend manages mip generation on the texture object itself.
779
   */
780
  generateMipmapsWebGPU(_texture: Texture): void {
781
    throw new Error('not implemented');
1✔
782
  }
783

784
  /** Internal helper for creating a shareable WebGL render-pipeline implementation. */
785
  _createSharedRenderPipelineWebGL(_props: RenderPipelineProps): SharedRenderPipeline {
786
    throw new Error('_createSharedRenderPipelineWebGL() not implemented');
×
787
  }
788

789
  /**
790
   * Internal helper that returns `true` when timestamp-query GPU timing should be
791
   * collected for this device.
792
   */
793
  _supportsDebugGPUTime(): boolean {
794
    return (
12✔
795
      this.features.has('timestamp-query') && Boolean(this.props.debug || this.props.debugGPUTime)
37✔
796
    );
797
  }
798

799
  /**
800
   * Internal helper that enables device-managed GPU timing collection on the
801
   * default command encoder. Reuses the existing query set if timing is already enabled.
802
   *
803
   * @param queryCount - Number of timestamp slots reserved for profiled passes.
804
   * @returns The device-managed timestamp QuerySet, or `null` when timing is not supported or could not be enabled.
805
   */
806
  _enableDebugGPUTime(queryCount: number = 256): QuerySet | null {
11✔
807
    if (!this._supportsDebugGPUTime()) {
11!
808
      return null;
×
809
    }
810

811
    if (this._debugGPUTimeQuery) {
11✔
812
      return this._debugGPUTimeQuery;
6✔
813
    }
814

815
    try {
5✔
816
      this._debugGPUTimeQuery = this.createQuerySet({type: 'timestamp', count: queryCount});
5✔
817
      this.commandEncoder = this.createCommandEncoder({
5✔
818
        id: this.commandEncoder.props.id,
819
        timeProfilingQuerySet: this._debugGPUTimeQuery
820
      });
821
    } catch {
822
      this._debugGPUTimeQuery = null;
×
823
    }
824

825
    return this._debugGPUTimeQuery;
5✔
826
  }
827

828
  /**
829
   * Internal helper that disables device-managed GPU timing collection and restores
830
   * the default command encoder to an unprofiled state.
831
   */
832
  _disableDebugGPUTime(): void {
833
    if (!this._debugGPUTimeQuery) {
6✔
834
      return;
1✔
835
    }
836

837
    if (this.commandEncoder.getTimeProfilingQuerySet() === this._debugGPUTimeQuery) {
5!
838
      this.commandEncoder = this.createCommandEncoder({
5✔
839
        id: this.commandEncoder.props.id
840
      });
841
    }
842

843
    this._debugGPUTimeQuery.destroy();
5✔
844
    this._debugGPUTimeQuery = null;
5✔
845
  }
846

847
  /** Internal helper that returns `true` when device-managed GPU timing is currently active. */
848
  _isDebugGPUTimeEnabled(): boolean {
849
    return this._debugGPUTimeQuery !== null;
43✔
850
  }
851

852
  /**
853
   * Determines what operations are supported on a texture format, checking against supported device features
854
   * Subclasses override to apply additional checks
855
   */
856
  protected abstract _getDeviceSpecificTextureFormatCapabilities(
857
    format: DeviceTextureFormatCapabilities
858
  ): DeviceTextureFormatCapabilities;
859

860
  // DEPRECATED METHODS
861

862
  /** @deprecated Use getDefaultCanvasContext() */
863
  getCanvasContext(): CanvasContext {
864
    return this.getDefaultCanvasContext();
32✔
865
  }
866

867
  // WebGL specific HACKS - enables app to remove webgl import
868
  // Use until we have a better way to handle these
869

870
  /** @deprecated - will be removed - should use command encoder */
871
  readPixelsToArrayWebGL(
872
    source: Framebuffer | Texture,
873
    options?: {
874
      sourceX?: number;
875
      sourceY?: number;
876
      sourceFormat?: number;
877
      sourceAttachment?: number;
878
      target?: Uint8Array | Uint16Array | Float32Array;
879
      // following parameters are auto deduced if not provided
880
      sourceWidth?: number;
881
      sourceHeight?: number;
882
      sourceType?: number;
883
    }
884
  ): Uint8Array | Uint16Array | Float32Array {
885
    throw new Error('not implemented');
×
886
  }
887

888
  /** @deprecated - will be removed - should use command encoder */
889
  readPixelsToBufferWebGL(
890
    source: Framebuffer | Texture,
891
    options?: {
892
      sourceX?: number;
893
      sourceY?: number;
894
      sourceFormat?: number;
895
      target?: Buffer; // A new Buffer object is created when not provided.
896
      targetByteOffset?: number; // byte offset in buffer object
897
      // following parameters are auto deduced if not provided
898
      sourceWidth?: number;
899
      sourceHeight?: number;
900
      sourceType?: number;
901
    }
902
  ): Buffer {
903
    throw new Error('not implemented');
×
904
  }
905

906
  /** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
907
  setParametersWebGL(parameters: any): void {
908
    throw new Error('not implemented');
×
909
  }
910

911
  /** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
912
  getParametersWebGL(parameters: any): void {
913
    throw new Error('not implemented');
×
914
  }
915

916
  /** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
917
  withParametersWebGL(parameters: any, func: any): any {
918
    throw new Error('not implemented');
×
919
  }
920

921
  /** @deprecated - will be removed - should use clear arguments in RenderPass */
922
  clearWebGL(options?: {framebuffer?: Framebuffer; color?: any; depth?: any; stencil?: any}): void {
923
    throw new Error('not implemented');
×
924
  }
925

926
  /** @deprecated - will be removed - should use for debugging only */
927
  resetWebGL(): void {
928
    throw new Error('not implemented');
×
929
  }
930

931
  // INTERNAL LUMA.GL METHODS
932

933
  getModuleData<ModuleDataT extends Record<string, unknown>>(moduleName: string): ModuleDataT {
934
    this._moduleData[moduleName] ||= {};
131✔
935
    return this._moduleData[moduleName] as ModuleDataT;
131✔
936
  }
937

938
  // INTERNAL HELPERS
939

940
  // IMPLEMENTATION
941

942
  /** Helper to get the canvas context props */
943
  static _getCanvasContextProps(props: DeviceProps): CanvasContextProps | undefined {
944
    return props.createCanvasContext === true ? {} : props.createCanvasContext;
147✔
945
  }
946

947
  protected _getDeviceTextureFormatCapabilities(
948
    format: TextureFormat
949
  ): DeviceTextureFormatCapabilities {
950
    const genericCapabilities = textureFormatDecoder.getCapabilities(format);
176✔
951

952
    // Check standard features
953
    const checkFeature = (feature: DeviceFeature | boolean | undefined) =>
852!
954
      (typeof feature === 'string' ? this.features.has(feature) : feature) ?? true;
852✔
955

956
    const supported = checkFeature(genericCapabilities.create);
176✔
957
    return {
176✔
958
      format,
959
      create: supported,
960
      render: supported && checkFeature(genericCapabilities.render),
345✔
961
      filter: supported && checkFeature(genericCapabilities.filter),
345✔
962
      blend: supported && checkFeature(genericCapabilities.blend),
345✔
963
      store: supported && checkFeature(genericCapabilities.store)
345✔
964
    } as const satisfies DeviceTextureFormatCapabilities;
965
  }
966

967
  /** Subclasses use this to support .createBuffer() overloads */
968
  protected _normalizeBufferProps(props: BufferProps | ArrayBuffer | ArrayBufferView): BufferProps {
969
    if (props instanceof ArrayBuffer || ArrayBuffer.isView(props)) {
538!
970
      props = {data: props};
×
971
    }
972

973
    // TODO(ibgreen) - fragile, as this is done before we merge with default options
974
    // inside the Buffer constructor
975

976
    const newProps = {...props};
538✔
977
    // Deduce indexType
978
    const usage = props.usage || 0;
538✔
979
    if (usage & Buffer.INDEX) {
538✔
980
      if (!props.indexType) {
26!
981
        if (props.data instanceof Uint32Array) {
26✔
982
          newProps.indexType = 'uint32';
1✔
983
        } else if (props.data instanceof Uint16Array) {
25✔
984
          newProps.indexType = 'uint16';
22✔
985
        } else if (props.data instanceof Uint8Array) {
3!
986
          // Convert uint8 to uint16 for WebGPU compatibility (WebGPU doesn't support uint8 indices)
987
          newProps.data = new Uint16Array(props.data);
3✔
988
          newProps.indexType = 'uint16';
3✔
989
        }
990
      }
991
      if (!newProps.indexType) {
26!
992
        throw new Error('indices buffer content must be of type uint16 or uint32');
×
993
      }
994
    }
995

996
    return newProps;
538✔
997
  }
998
}
999

1000
/**
1001
 * Internal helper for resolving the default `debug` prop.
1002
 * Precedence is: explicit log debug value first, then `NODE_ENV`, then `false`.
1003
 */
1004
export function _getDefaultDebugValue(logDebugValue: unknown, nodeEnv?: string): boolean {
1005
  if (logDebugValue !== undefined && logDebugValue !== null) {
118✔
1006
    return Boolean(logDebugValue);
2✔
1007
  }
1008

1009
  if (nodeEnv !== undefined) {
116✔
1010
    return nodeEnv !== 'production';
2✔
1011
  }
1012

1013
  return false;
114✔
1014
}
1015

1016
function getDefaultDebugValue(): boolean {
1017
  return _getDefaultDebugValue(log.get('debug'), getNodeEnv());
113✔
1018
}
1019

1020
function getNodeEnv(): string | undefined {
1021
  const processObject = (
1022
    globalThis as typeof globalThis & {
113✔
1023
      process?: {env?: Record<string, string | undefined>};
1024
    }
1025
  ).process;
1026
  if (!processObject?.env) {
113!
1027
    return undefined;
113✔
1028
  }
1029

1030
  return processObject.env['NODE_ENV'];
×
1031
}
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