@luma.gl/core
Version:
The luma.gl core Device API
1,059 lines (912 loc) • 40.4 kB
text/typescript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {StatsManager, lumaStats} from '../utils/stats-manager';
import {log} from '../utils/log';
import {uid} from '../utils/uid';
import type {VertexFormat, VertexFormatInfo} from '../shadertypes/vertex-types/vertex-formats';
import type {
TextureFormat,
TextureFormatInfo,
CompressedTextureFormat
} from '../shadertypes/texture-types/texture-formats';
import type {CanvasContext, CanvasContextProps} from './canvas-context';
import type {PresentationContext, PresentationContextProps} from './presentation-context';
import type {BufferProps} from './resources/buffer';
import {Buffer} from './resources/buffer';
import type {RenderPipeline, RenderPipelineProps} from './resources/render-pipeline';
import type {SharedRenderPipeline} from './resources/shared-render-pipeline';
import type {ComputePipeline, ComputePipelineProps} from './resources/compute-pipeline';
import type {Sampler, SamplerProps} from './resources/sampler';
import type {Shader, ShaderProps} from './resources/shader';
import type {Texture, TextureProps} from './resources/texture';
import type {ExternalTexture, ExternalTextureProps} from './resources/external-texture';
import type {Framebuffer, FramebufferProps} from './resources/framebuffer';
import type {RenderPass, RenderPassProps} from './resources/render-pass';
import type {ComputePass, ComputePassProps} from './resources/compute-pass';
import type {CommandEncoder, CommandEncoderProps} from './resources/command-encoder';
import type {CommandBuffer} from './resources/command-buffer';
import type {VertexArray, VertexArrayProps} from './resources/vertex-array';
import type {TransformFeedback, TransformFeedbackProps} from './resources/transform-feedback';
import type {QuerySet, QuerySetProps} from './resources/query-set';
import type {Fence} from './resources/fence';
import type {Bindings, ComputeShaderLayout, ShaderLayout} from './types/shader-layout';
import {vertexFormatDecoder} from '../shadertypes/vertex-types/vertex-format-decoder';
import {textureFormatDecoder} from '../shadertypes/texture-types/texture-format-decoder';
import type {ExternalImage} from '../shadertypes/image-types/image-types';
import {isExternalImage, getExternalImageSize} from '../shadertypes/image-types/image-types';
import {getTextureFormatTable} from '../shadertypes/texture-types/texture-format-table';
/**
* Identifies the GPU vendor and driver.
* @note Chrome WebGPU does not provide much information, though more can be enabled with
* @see https://developer.chrome.com/blog/new-in-webgpu-120#adapter_information_updates
* chrome://flags/#enable-webgpu-developer-features
*/
export type DeviceInfo = {
/** Type of device */
type: 'webgl' | 'webgpu' | 'null' | 'unknown';
/** Vendor (name of GPU vendor, Apple, nVidia etc */
vendor: string;
/** Renderer (usually driver name) */
renderer: string;
/** version of driver */
version: string;
/** family of GPU */
gpu: 'nvidia' | 'amd' | 'intel' | 'apple' | 'software' | 'unknown';
/** Type of GPU () */
gpuType: 'discrete' | 'integrated' | 'cpu' | 'unknown';
/** GPU architecture */
gpuArchitecture?: string; // 'common-3' on Apple
/** GPU driver backend. Can sometimes be sniffed */
gpuBackend?: 'opengl' | 'opengles' | 'metal' | 'd3d11' | 'd3d12' | 'vulkan' | 'unknown';
/** If this is a fallback adapter */
fallback?: boolean;
/** Shader language supported by device.createShader() */
shadingLanguage: 'wgsl' | 'glsl';
/** Highest supported shader language version: GLSL 3.00 = 300, WGSL 1.00 = 100 */
shadingLanguageVersion: number;
};
/** Limits for a device (max supported sizes of resources, max number of bindings etc) */
export abstract class DeviceLimits {
/** max number of TextureDimension1D */
abstract maxTextureDimension1D: number;
/** max number of TextureDimension2D */
abstract maxTextureDimension2D: number;
/** max number of TextureDimension3D */
abstract maxTextureDimension3D: number;
/** max number of TextureArrayLayers */
abstract maxTextureArrayLayers: number;
/** max number of BindGroups */
abstract maxBindGroups: number;
/** max number of DynamicUniformBuffers per PipelineLayout */
abstract maxDynamicUniformBuffersPerPipelineLayout: number;
/** max number of DynamicStorageBuffers per PipelineLayout */
abstract maxDynamicStorageBuffersPerPipelineLayout: number;
/** max number of SampledTextures per ShaderStage */
abstract maxSampledTexturesPerShaderStage: number;
/** max number of Samplers per ShaderStage */
abstract maxSamplersPerShaderStage: number;
/** max number of StorageBuffers per ShaderStage */
abstract maxStorageBuffersPerShaderStage: number;
/** max number of StorageTextures per ShaderStage */
abstract maxStorageTexturesPerShaderStage: number;
/** max number of UniformBuffers per ShaderStage */
abstract maxUniformBuffersPerShaderStage: number;
/** max number of UniformBufferBindingSize */
abstract maxUniformBufferBindingSize: number;
/** max number of StorageBufferBindingSize */
abstract maxStorageBufferBindingSize: number;
/** min UniformBufferOffsetAlignment */
abstract minUniformBufferOffsetAlignment: number;
/** min StorageBufferOffsetAlignment */
abstract minStorageBufferOffsetAlignment: number;
/** max number of VertexBuffers */
abstract maxVertexBuffers: number;
/** max number of VertexAttributes */
abstract maxVertexAttributes: number;
/** max number of VertexBufferArrayStride */
abstract maxVertexBufferArrayStride: number;
/** max number of InterStageShaderComponents */
abstract maxInterStageShaderVariables: number;
/** max number of ComputeWorkgroupStorageSize */
abstract maxComputeWorkgroupStorageSize: number;
/** max number of ComputeInvocations per Workgroup */
abstract maxComputeInvocationsPerWorkgroup: number;
/** max ComputeWorkgroupSizeX */
abstract maxComputeWorkgroupSizeX: number;
/** max ComputeWorkgroupSizeY */
abstract maxComputeWorkgroupSizeY: number;
/** max ComputeWorkgroupSizeZ */
abstract maxComputeWorkgroupSizeZ: number;
/** max ComputeWorkgroupsPerDimension */
abstract maxComputeWorkgroupsPerDimension: number;
}
function formatErrorLogArguments(context: unknown, args: unknown[]): unknown[] {
const formattedContext = formatErrorLogValue(context);
const formattedArgs = args.map(formatErrorLogValue).filter(arg => arg !== undefined);
return [formattedContext, ...formattedArgs].filter(arg => arg !== undefined);
}
function formatErrorLogValue(value: unknown): unknown {
if (value === undefined) {
return undefined;
}
if (
value === null ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value;
}
if (value instanceof Error) {
return value.message;
}
if (Array.isArray(value)) {
return value.map(formatErrorLogValue);
}
if (typeof value === 'object') {
if (hasCustomToString(value)) {
const stringValue = String(value);
if (stringValue !== '[object Object]') {
return stringValue;
}
}
if (looksLikeGPUCompilationMessage(value)) {
return formatGPUCompilationMessage(value);
}
return value.constructor?.name || 'Object';
}
return String(value);
}
function hasCustomToString(value: object): boolean {
return (
'toString' in value &&
typeof value.toString === 'function' &&
value.toString !== Object.prototype.toString
);
}
function looksLikeGPUCompilationMessage(value: object): value is {
message?: unknown;
type?: unknown;
lineNum?: unknown;
linePos?: unknown;
} {
return 'message' in value && 'type' in value;
}
function formatGPUCompilationMessage(value: {
message?: unknown;
type?: unknown;
lineNum?: unknown;
linePos?: unknown;
}): string {
const type = typeof value.type === 'string' ? value.type : 'message';
const message = typeof value.message === 'string' ? value.message : '';
const lineNum = typeof value.lineNum === 'number' ? value.lineNum : null;
const linePos = typeof value.linePos === 'number' ? value.linePos : null;
const location =
lineNum !== null && linePos !== null
? ` @ ${lineNum}:${linePos}`
: lineNum !== null
? ` @ ${lineNum}`
: '';
return `${type}${location}: ${message}`.trim();
}
/** Set-like class for features (lets apps check for WebGL / WebGPU extensions) */
export class DeviceFeatures {
protected features: Set<DeviceFeature>;
protected disabledFeatures?: Partial<Record<DeviceFeature, boolean>>;
constructor(
features: DeviceFeature[] = [],
disabledFeatures: Partial<Record<DeviceFeature, boolean>>
) {
this.features = new Set<DeviceFeature>(features);
this.disabledFeatures = disabledFeatures || {};
}
*[Symbol.iterator](): IterableIterator<DeviceFeature> {
yield* this.features;
}
has(feature: DeviceFeature): boolean {
return !this.disabledFeatures?.[feature] && this.features.has(feature);
}
}
/** Device feature names */
export type DeviceFeature =
| WebGPUDeviceFeature
| WebGLDeviceFeature
| WebGLCompressedTextureFeatures;
// | ChromeExperimentalFeatures
/** Chrome-specific extensions. Expected to eventually become standard features. */
// export type ChromeExperimentalFeatures = ;
export type WebGPUDeviceFeature =
| 'depth-clip-control'
| 'depth32float-stencil8'
| 'texture-compression-bc'
| 'texture-compression-bc-sliced-3d'
| 'texture-compression-etc2'
| 'texture-compression-astc'
| 'texture-compression-astc-sliced-3d'
| 'timestamp-query'
| 'indirect-first-instance'
| 'shader-f16'
| 'rg11b10ufloat-renderable' // Is the rg11b10ufloat texture format renderable?
| 'bgra8unorm-storage' // Can the bgra8unorm texture format be used in storage buffers?
| 'float32-filterable' // Is the float32 format filterable?
| 'float32-blendable' // Is the float32 format blendable?
| 'clip-distances'
| 'dual-source-blending'
| 'subgroups';
// | 'depth-clamping' // removed from the WebGPU spec...
// | 'pipeline-statistics-query' // removed from the WebGPU spec...
export type WebGLDeviceFeature =
// webgl extension features
| 'compilation-status-async-webgl' // Non-blocking shader compile/link status query available
| 'provoking-vertex-webgl' // parameters.provokingVertex
| 'polygon-mode-webgl' // parameters.polygonMode and parameters.polygonOffsetLine
// GLSL extension features
| 'shader-noperspective-interpolation-webgl' // Vertex outputs & fragment inputs can have a `noperspective` interpolation qualifier.
| 'shader-conservative-depth-webgl' // GLSL `gl_FragDepth` qualifiers `depth_unchanged` etc can enable early depth test
| 'shader-clip-cull-distance-webgl' // Makes gl_ClipDistance and gl_CullDistance available in shaders
// texture rendering
| 'float32-renderable-webgl'
| 'float16-renderable-webgl'
| 'rgb9e5ufloat-renderable-webgl'
| 'snorm8-renderable-webgl'
| 'norm16-webgl'
| 'norm16-renderable-webgl'
| 'snorm16-renderable-webgl'
// texture filtering
| 'float16-filterable-webgl'
| 'texture-filterable-anisotropic-webgl'
// texture storage bindings
| 'bgra8unorm-storage'
// texture blending
| 'texture-blend-float-webgl';
type WebGLCompressedTextureFeatures =
| 'texture-compression-bc5-webgl'
| 'texture-compression-bc7-webgl'
| 'texture-compression-etc1-webgl'
| 'texture-compression-pvrtc-webgl'
| 'texture-compression-atc-webgl';
/** Texture format capabilities that have been checked against a specific device */
export type DeviceTextureFormatCapabilities = {
format: TextureFormat;
/** Can the format be created and sampled?*/
create: boolean;
/** Is the format renderable. */
render: boolean;
/** Is the format filterable. */
filter: boolean;
/** Is the format blendable. */
blend: boolean;
/** Is the format storeable. */
store: boolean;
};
/** Device properties */
export type DeviceProps = {
/** string id for debugging. Stored on the object, used in logging and set on underlying GPU objects when feasible. */
id?: string;
/** Properties for creating a default canvas context */
createCanvasContext?: CanvasContextProps | true;
/** Control which type of GPU is preferred on systems with both integrated and discrete GPU. Defaults to "high-performance" / discrete GPU. */
powerPreference?: 'default' | 'high-performance' | 'low-power';
/** Hints that device creation should fail if no hardware GPU is available (if the system performance is "low"). */
failIfMajorPerformanceCaveat?: boolean;
/** WebGL specific: Properties passed through to WebGL2RenderingContext creation: `canvas.getContext('webgl2', props.webgl)` */
webgl?: WebGLContextProps;
// CALLBACKS
/** 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. */
onError?: (error: Error, context?: unknown) => unknown;
/** Called when the size of a CanvasContext's canvas changes */
onResize?: (
ctx: CanvasContext | PresentationContext,
info: {oldPixelSize: [number, number]}
) => unknown;
/** Called when the absolute position of a CanvasContext's canvas changes. Must set `CanvasContextProps.trackPosition: true` */
onPositionChange?: (
ctx: CanvasContext | PresentationContext,
info: {oldPosition: [number, number]}
) => unknown;
/** Called when the visibility of a CanvasContext's canvas changes */
onVisibilityChange?: (ctx: CanvasContext | PresentationContext) => unknown;
/** Called when the device pixel ratio of a CanvasContext's canvas changes */
onDevicePixelRatioChange?: (
ctx: CanvasContext | PresentationContext,
info: {oldRatio: number}
) => unknown;
// DEBUG SETTINGS
/** Turn on implementation defined checks that slow down execution but help break where errors occur */
debug?: boolean;
/** Enable GPU timestamp collection without enabling all debug validation paths. */
debugGPUTime?: boolean;
/** Show shader source in browser? The default is `'error'`, meaning that logs are shown when shader compilation has errors */
debugShaders?: 'never' | 'errors' | 'warnings' | 'always';
/** Renders a small version of updated Framebuffers into the primary canvas context. Can be set in console luma.log.set('debug-framebuffers', true) */
debugFramebuffers?: boolean;
/** Traces resource caching, reuse, and destroys in the PipelineFactory */
debugFactories?: boolean;
/** WebGL specific - Trace WebGL calls (instruments WebGL2RenderingContext at the expense of performance). Can be set in console luma.log.set('debug-webgl', true) */
debugWebGL?: boolean;
/** WebGL specific - Initialize the SpectorJS WebGL debugger. Can be set in console luma.log.set('debug-spectorjs', true) */
debugSpectorJS?: boolean;
/** WebGL specific - SpectorJS URL. Override if CDN is down or different SpectorJS version is desired. */
debugSpectorJSUrl?: string;
// EXPERIMENTAL SETTINGS - subject to change
/** adapter.create() returns the existing Device if the provided canvas' WebGL context is already associated with a Device. */
_reuseDevices?: boolean;
/** WebGPU specific - Request a Device with the highest limits supported by platform. On WebGPU devices can be created with minimal limits. */
_requestMaxLimits?: boolean;
/** Disable specific features */
_disabledFeatures?: Partial<Record<DeviceFeature, boolean>>;
/** WebGL specific - Initialize all features on startup */
_initializeFeatures?: boolean;
/** Enable shader caching (via ShaderFactory) */
_cacheShaders?: boolean;
/**
* Destroy cached shaders when they become unused.
* Defaults to `false` so repeated create/destroy cycles can still reuse cached shaders.
* Enable this if the application creates very large numbers of distinct shaders and needs cache eviction.
*/
_destroyShaders?: boolean;
/** Enable pipeline caching (via PipelineFactory) */
_cachePipelines?: boolean;
/** Enable sharing of backend render-pipeline implementations when caching is enabled. Currently used by WebGL. */
_sharePipelines?: boolean;
/**
* Destroy cached pipelines when they become unused.
* Defaults to `false` so repeated create/destroy cycles can still reuse cached pipelines.
* Enable this if the application creates very large numbers of distinct pipelines and needs cache eviction.
*/
_destroyPipelines?: boolean;
/** @deprecated Internal, Do not use directly! Use `luma.attachDevice()` to attach to pre-created contexts/devices. */
_handle?: unknown; // WebGL2RenderingContext | GPUDevice | null;
};
type DeviceFactories = {
bindGroupFactory?: unknown;
};
/** WebGL independent copy of WebGLContextAttributes */
type WebGLContextProps = {
/** indicates if the canvas contains an alpha buffer. */
alpha?: boolean;
/** hints the user agent to reduce the latency by desynchronizing the canvas paint cycle from the event loop */
desynchronized?: boolean;
/** indicates whether or not to perform anti-aliasing. */
antialias?: boolean;
/** indicates that the render target has a stencil buffer of at least `8` bits. */
stencil?: boolean;
/** indicates that the drawing buffer has a depth buffer of at least 16 bits. */
depth?: boolean;
/** indicates if a context will be created if the system performance is low or if no hardware GPU is available. */
failIfMajorPerformanceCaveat?: boolean;
/** Selects GPU */
powerPreference?: 'default' | 'high-performance' | 'low-power';
/** page compositor will assume the drawing buffer contains colors with pre-multiplied alpha. */
premultipliedAlpha?: boolean;
/** buffers will not be cleared and will preserve their values until cleared or overwritten by the author. */
preserveDrawingBuffer?: boolean;
};
/**
* Create and attach devices for a specific backend. Currently static methods on each device
*/
export interface DeviceFactory {
// new (props: DeviceProps): Device; Constructor isn't used
type: string;
isSupported(): boolean;
create(props: DeviceProps): Promise<Device>;
attach?(handle: unknown): Device;
}
/**
* WebGPU Device/WebGL context abstraction
*/
export abstract class Device {
static defaultProps: Required<DeviceProps> = {
id: null!,
powerPreference: 'high-performance',
failIfMajorPerformanceCaveat: false,
createCanvasContext: undefined!,
// WebGL specific
webgl: {},
// Callbacks
// eslint-disable-next-line handle-callback-err
onError: (error: Error, context: unknown) => {},
onResize: (context: CanvasContext, info: {oldPixelSize: [number, number]}) => {
const [width, height] = context.getDevicePixelSize();
log.log(1, `${context} resized => ${width}x${height}px`)();
},
onPositionChange: (context: CanvasContext, info: {oldPosition: [number, number]}) => {
const [left, top] = context.getPosition();
log.log(1, `${context} repositioned => ${left},${top}`)();
},
onVisibilityChange: (context: CanvasContext) =>
log.log(1, `${context} Visibility changed ${context.isVisible}`)(),
onDevicePixelRatioChange: (context: CanvasContext, info: {oldRatio: number}) =>
log.log(1, `${context} DPR changed ${info.oldRatio} => ${context.devicePixelRatio}`)(),
// Debug flags
debug: getDefaultDebugValue(),
debugGPUTime: false,
debugShaders: log.get('debug-shaders') || undefined!,
debugFramebuffers: Boolean(log.get('debug-framebuffers')),
debugFactories: Boolean(log.get('debug-factories')),
debugWebGL: Boolean(log.get('debug-webgl')),
debugSpectorJS: undefined!, // Note: log setting is queried by the spector.js code
debugSpectorJSUrl: undefined!,
// Experimental
_reuseDevices: false,
_requestMaxLimits: true,
_cacheShaders: true,
_destroyShaders: false,
_cachePipelines: true,
_sharePipelines: true,
_destroyPipelines: false,
// TODO - Change these after confirming things work as expected
_initializeFeatures: true,
_disabledFeatures: {
'compilation-status-async-webgl': true
},
// INTERNAL
_handle: undefined!
};
get [Symbol.toStringTag](): string {
return 'Device';
}
toString(): string {
return `Device(${this.id})`;
}
/** id of this device, primarily for debugging */
readonly id: string;
/** type of this device */
abstract readonly type: 'webgl' | 'webgpu' | 'null' | 'unknown';
abstract readonly handle: unknown;
abstract commandEncoder: CommandEncoder;
/** A copy of the device props */
readonly props: Required<DeviceProps>;
/** Available for the application to store data on the device */
userData: {[key: string]: unknown} = {};
/** stats */
readonly statsManager: StatsManager = lumaStats;
/** Internal per-device factory storage */
_factories: DeviceFactories = {};
/** An abstract timestamp used for change tracking */
timestamp: number = 0;
/** True if this device has been reused during device creation (app has multiple references) */
_reused: boolean = false;
/** Used by other luma.gl modules to store data on the device */
private _moduleData: Record<string, Record<string, unknown>> = {};
// Capabilities
/** Information about the device (vendor, versions etc) */
abstract info: DeviceInfo;
/** Optional capability discovery */
abstract features: DeviceFeatures;
/** WebGPU style device limits */
abstract get limits(): DeviceLimits;
// Texture helpers
/** Optimal TextureFormat for displaying 8-bit depth, standard dynamic range content on this system. */
abstract preferredColorFormat: 'rgba8unorm' | 'bgra8unorm';
/** Default depth format used on this system */
abstract preferredDepthFormat: 'depth16' | 'depth24plus' | 'depth32float';
protected _textureCaps: Partial<Record<TextureFormat, DeviceTextureFormatCapabilities>> = {};
/** Internal timestamp query set used when GPU timing collection is enabled for this device. */
protected _debugGPUTimeQuery: QuerySet | null = null;
constructor(props: DeviceProps) {
this.props = {...Device.defaultProps, ...props};
this.id = this.props.id || uid(this[Symbol.toStringTag].toLowerCase());
}
abstract destroy(): void;
// TODO - just expose the shadertypes decoders?
getVertexFormatInfo(format: VertexFormat): VertexFormatInfo {
return vertexFormatDecoder.getVertexFormatInfo(format);
}
isVertexFormatSupported(format: VertexFormat): boolean {
return true;
}
/** Returns information about a texture format, such as data type, channels, bits per channel, compression etc */
getTextureFormatInfo(format: TextureFormat): TextureFormatInfo {
return textureFormatDecoder.getInfo(format);
}
/** Determines what operations are supported on a texture format on this particular device (checks against supported device features) */
getTextureFormatCapabilities(format: TextureFormat): DeviceTextureFormatCapabilities {
let textureCaps = this._textureCaps[format];
if (!textureCaps) {
const capabilities = this._getDeviceTextureFormatCapabilities(format);
textureCaps = this._getDeviceSpecificTextureFormatCapabilities(capabilities);
this._textureCaps[format] = textureCaps;
}
return textureCaps;
}
/** Calculates the number of mip levels for a texture of width, height and in case of 3d textures only, depth */
getMipLevelCount(width: number, height: number, depth3d: number = 1): number {
const maxSize = Math.max(width, height, depth3d);
return 1 + Math.floor(Math.log2(maxSize));
}
/** Check if data is an external image */
isExternalImage(data: unknown): data is ExternalImage {
return isExternalImage(data);
}
/** Get the size of an external image */
getExternalImageSize(data: ExternalImage): {width: number; height: number} {
return getExternalImageSize(data);
}
/** Check if device supports a specific texture format (creation and `nearest` sampling) */
isTextureFormatSupported(format: TextureFormat): boolean {
return this.getTextureFormatCapabilities(format).create;
}
/** Check if linear filtering (sampler interpolation) is supported for a specific texture format */
isTextureFormatFilterable(format: TextureFormat): boolean {
return this.getTextureFormatCapabilities(format).filter;
}
/** Check if device supports rendering to a framebuffer color attachment of a specific texture format */
isTextureFormatRenderable(format: TextureFormat): boolean {
return this.getTextureFormatCapabilities(format).render;
}
/** Check if a specific texture format is GPU compressed */
isTextureFormatCompressed(format: TextureFormat): boolean {
return textureFormatDecoder.isCompressed(format);
}
/** Returns the compressed texture formats that can be created and sampled on this device */
getSupportedCompressedTextureFormats(): CompressedTextureFormat[] {
const supportedFormats: CompressedTextureFormat[] = [];
for (const format of Object.keys(getTextureFormatTable()) as TextureFormat[]) {
if (this.isTextureFormatCompressed(format) && this.isTextureFormatSupported(format)) {
supportedFormats.push(format as CompressedTextureFormat);
}
}
return supportedFormats;
}
// DEBUG METHODS
pushDebugGroup(groupLabel: string): void {
this.commandEncoder.pushDebugGroup(groupLabel);
}
popDebugGroup(): void {
this.commandEncoder?.popDebugGroup();
}
insertDebugMarker(markerLabel: string): void {
this.commandEncoder?.insertDebugMarker(markerLabel);
}
// Device loss
/** `true` if device is already lost */
abstract get isLost(): boolean;
/** Promise that resolves when device is lost */
abstract readonly lost: Promise<{reason: 'destroyed'; message: string}>;
/**
* Trigger device loss.
* @returns `true` if context loss could actually be triggered.
* @note primarily intended for testing how application reacts to device loss
*/
loseDevice(): boolean {
return false;
}
/** A monotonic counter for tracking buffer and texture updates */
incrementTimestamp(): number {
return this.timestamp++;
}
/**
* Reports Device errors in a way that optimizes for developer experience / debugging.
* - Logs so that the console error links directly to the source code that generated the error.
* - Includes the object that reported the error in the log message, even if the error is asynchronous.
*
* Conventions when calling reportError():
* - Always call the returned function - to ensure error is logged, at the error site
* - Follow with a call to device.debug() - to ensure that the debugger breaks at the error site
*
* @param error - the error to report. If needed, just create a new Error object with the appropriate message.
* @param context - pass `this` as context, otherwise it may not be available in the debugger for async errors.
* @returns the logger function returned by device.props.onError() so that it can be called from the error site.
*
* @example
* device.reportError(new Error(...), this)();
* device.debug();
*/
reportError(error: Error, context: unknown, ...args: unknown[]): () => unknown {
// Call the error handler
const isHandled = this.props.onError(error, context);
if (!isHandled) {
const logArguments = formatErrorLogArguments(context, args);
// Note: Returns a function that must be called: `device.reportError(...)()`
return log.error(
this.type === 'webgl' ? '%cWebGL' : '%cWebGPU',
'color: white; background: red; padding: 2px 6px; border-radius: 3px;',
error.message,
...logArguments
);
}
return () => {};
}
/** Break in the debugger - if device.props.debug is true */
debug(): void {
if (this.props.debug) {
// @ts-ignore
// biome-ignore lint/suspicious/noDebugger: explicit debug break when device debugging is enabled.
debugger;
} else {
// TODO(ibgreen): Does not appear to be printed in the console
const message = `\
'Type luma.log.set({debug: true}) in console to enable debug breakpoints',
or create a device with the 'debug: true' prop.`;
log.once(0, message)();
}
}
// Canvas context
/** Default / primary canvas context. Can be null as WebGPU devices can be created without a CanvasContext */
abstract canvasContext: CanvasContext | null;
/** Returns the default / primary canvas context. Throws an error if no canvas context is available (a WebGPU compute device) */
getDefaultCanvasContext(): CanvasContext {
if (!this.canvasContext) {
throw new Error('Device has no default CanvasContext. See props.createCanvasContext');
}
return this.canvasContext;
}
/** Creates a new CanvasContext (WebGPU only) */
abstract createCanvasContext(props?: CanvasContextProps): CanvasContext;
/** Creates a presentation context for a destination canvas. WebGL requires the default canvas context to use an OffscreenCanvas. */
abstract createPresentationContext(props?: PresentationContextProps): PresentationContext;
/** Call after rendering a frame (necessary e.g. on WebGL OffscreenCanvas) */
abstract submit(commandBuffer?: CommandBuffer): void;
// Resource creation
/** Create a buffer */
abstract createBuffer(props: BufferProps | ArrayBuffer | ArrayBufferView): Buffer;
/** Create a texture */
abstract createTexture(props: TextureProps): Texture;
/** Create a temporary texture view of a video source */
abstract createExternalTexture(props: ExternalTextureProps): ExternalTexture;
/** Create a sampler */
abstract createSampler(props: SamplerProps): Sampler;
/** Create a Framebuffer. Must have at least one attachment. */
abstract createFramebuffer(props: FramebufferProps): Framebuffer;
/** Create a shader */
abstract createShader(props: ShaderProps): Shader;
/** Create a render pipeline (aka program) */
abstract createRenderPipeline(props: RenderPipelineProps): RenderPipeline;
/** Create a compute pipeline (aka program). WebGPU only. */
abstract createComputePipeline(props: ComputePipelineProps): ComputePipeline;
/** Create a vertex array */
abstract createVertexArray(props: VertexArrayProps): VertexArray;
abstract createCommandEncoder(props?: CommandEncoderProps): CommandEncoder;
/** Create a transform feedback (immutable set of output buffer bindings). WebGL only. */
abstract createTransformFeedback(props: TransformFeedbackProps): TransformFeedback;
abstract createQuerySet(props: QuerySetProps): QuerySet;
/** Create a fence sync object */
createFence(): Fence {
throw new Error('createFence() not implemented');
}
/** Create a RenderPass using the default CommandEncoder */
beginRenderPass(props?: RenderPassProps): RenderPass {
return this.commandEncoder.beginRenderPass(props);
}
/** Create a ComputePass using the default CommandEncoder*/
beginComputePass(props?: ComputePassProps): ComputePass {
return this.commandEncoder.beginComputePass(props);
}
/**
* Generate mipmaps for a WebGPU texture.
* WebGPU textures must be created up front with the required mip count, usage flags, and a format that supports the chosen generation path.
* WebGL uses `Texture.generateMipmapsWebGL()` directly because the backend manages mip generation on the texture object itself.
*/
generateMipmapsWebGPU(_texture: Texture): void {
throw new Error('not implemented');
}
/** Internal helper for creating a shareable WebGL render-pipeline implementation. */
_createSharedRenderPipelineWebGL(_props: RenderPipelineProps): SharedRenderPipeline {
throw new Error('_createSharedRenderPipelineWebGL() not implemented');
}
/** Internal WebGPU-only helper for retrieving the native bind-group layout for a pipeline group. */
_createBindGroupLayoutWebGPU(
_pipeline: RenderPipeline | ComputePipeline,
_group: number
): unknown {
throw new Error('_createBindGroupLayoutWebGPU() not implemented');
}
/** Internal WebGPU-only helper for creating a native bind group. */
_createBindGroupWebGPU(
_bindGroupLayout: unknown,
_shaderLayout: ShaderLayout | ComputeShaderLayout,
_bindings: Bindings,
_group: number,
_label?: string
): unknown {
throw new Error('_createBindGroupWebGPU() not implemented');
}
/**
* Internal helper that returns `true` when timestamp-query GPU timing should be
* collected for this device.
*/
_supportsDebugGPUTime(): boolean {
return (
this.features.has('timestamp-query') && Boolean(this.props.debug || this.props.debugGPUTime)
);
}
/**
* Internal helper that enables device-managed GPU timing collection on the
* default command encoder. Reuses the existing query set if timing is already enabled.
*
* @param queryCount - Number of timestamp slots reserved for profiled passes.
* @returns The device-managed timestamp QuerySet, or `null` when timing is not supported or could not be enabled.
*/
_enableDebugGPUTime(queryCount: number = 256): QuerySet | null {
if (!this._supportsDebugGPUTime()) {
return null;
}
if (this._debugGPUTimeQuery) {
return this._debugGPUTimeQuery;
}
try {
this._debugGPUTimeQuery = this.createQuerySet({type: 'timestamp', count: queryCount});
this.commandEncoder = this.createCommandEncoder({
id: this.commandEncoder.props.id,
timeProfilingQuerySet: this._debugGPUTimeQuery
});
} catch {
this._debugGPUTimeQuery = null;
}
return this._debugGPUTimeQuery;
}
/**
* Internal helper that disables device-managed GPU timing collection and restores
* the default command encoder to an unprofiled state.
*/
_disableDebugGPUTime(): void {
if (!this._debugGPUTimeQuery) {
return;
}
if (this.commandEncoder.getTimeProfilingQuerySet() === this._debugGPUTimeQuery) {
this.commandEncoder = this.createCommandEncoder({
id: this.commandEncoder.props.id
});
}
this._debugGPUTimeQuery.destroy();
this._debugGPUTimeQuery = null;
}
/** Internal helper that returns `true` when device-managed GPU timing is currently active. */
_isDebugGPUTimeEnabled(): boolean {
return this._debugGPUTimeQuery !== null;
}
/**
* Determines what operations are supported on a texture format, checking against supported device features
* Subclasses override to apply additional checks
*/
protected abstract _getDeviceSpecificTextureFormatCapabilities(
format: DeviceTextureFormatCapabilities
): DeviceTextureFormatCapabilities;
// DEPRECATED METHODS
/** @deprecated Use getDefaultCanvasContext() */
getCanvasContext(): CanvasContext {
return this.getDefaultCanvasContext();
}
// WebGL specific HACKS - enables app to remove webgl import
// Use until we have a better way to handle these
/** @deprecated - will be removed - should use command encoder */
readPixelsToArrayWebGL(
source: Framebuffer | Texture,
options?: {
sourceX?: number;
sourceY?: number;
sourceFormat?: number;
sourceAttachment?: number;
target?: Uint8Array | Uint16Array | Float32Array;
// following parameters are auto deduced if not provided
sourceWidth?: number;
sourceHeight?: number;
sourceType?: number;
}
): Uint8Array | Uint16Array | Float32Array {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use command encoder */
readPixelsToBufferWebGL(
source: Framebuffer | Texture,
options?: {
sourceX?: number;
sourceY?: number;
sourceFormat?: number;
target?: Buffer; // A new Buffer object is created when not provided.
targetByteOffset?: number; // byte offset in buffer object
// following parameters are auto deduced if not provided
sourceWidth?: number;
sourceHeight?: number;
sourceType?: number;
}
): Buffer {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
setParametersWebGL(parameters: any): void {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
getParametersWebGL(parameters: any): void {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
withParametersWebGL(parameters: any, func: any): any {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use clear arguments in RenderPass */
clearWebGL(options?: {framebuffer?: Framebuffer; color?: any; depth?: any; stencil?: any}): void {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use for debugging only */
resetWebGL(): void {
throw new Error('not implemented');
}
// INTERNAL LUMA.GL METHODS
getModuleData<ModuleDataT extends Record<string, unknown>>(moduleName: string): ModuleDataT {
this._moduleData[moduleName] ||= {};
return this._moduleData[moduleName] as ModuleDataT;
}
// INTERNAL HELPERS
// IMPLEMENTATION
/** Helper to get the canvas context props */
static _getCanvasContextProps(props: DeviceProps): CanvasContextProps | undefined {
return props.createCanvasContext === true ? {} : props.createCanvasContext;
}
protected _getDeviceTextureFormatCapabilities(
format: TextureFormat
): DeviceTextureFormatCapabilities {
const genericCapabilities = textureFormatDecoder.getCapabilities(format);
// Check standard features
const checkFeature = (feature: DeviceFeature | boolean | undefined) =>
(typeof feature === 'string' ? this.features.has(feature) : feature) ?? true;
const supported = checkFeature(genericCapabilities.create);
return {
format,
create: supported,
render: supported && checkFeature(genericCapabilities.render),
filter: supported && checkFeature(genericCapabilities.filter),
blend: supported && checkFeature(genericCapabilities.blend),
store: supported && checkFeature(genericCapabilities.store)
} as const satisfies DeviceTextureFormatCapabilities;
}
/** Subclasses use this to support .createBuffer() overloads */
protected _normalizeBufferProps(props: BufferProps | ArrayBuffer | ArrayBufferView): BufferProps {
if (props instanceof ArrayBuffer || ArrayBuffer.isView(props)) {
props = {data: props};
}
// TODO(ibgreen) - fragile, as this is done before we merge with default options
// inside the Buffer constructor
const newProps = {...props};
// Deduce indexType
const usage = props.usage || 0;
if (usage & Buffer.INDEX) {
if (!props.indexType) {
if (props.data instanceof Uint32Array) {
newProps.indexType = 'uint32';
} else if (props.data instanceof Uint16Array) {
newProps.indexType = 'uint16';
} else if (props.data instanceof Uint8Array) {
// Convert uint8 to uint16 for WebGPU compatibility (WebGPU doesn't support uint8 indices)
newProps.data = new Uint16Array(props.data);
newProps.indexType = 'uint16';
}
}
if (!newProps.indexType) {
throw new Error('indices buffer content must be of type uint16 or uint32');
}
}
return newProps;
}
}
/**
* Internal helper for resolving the default `debug` prop.
* Precedence is: explicit log debug value first, then `NODE_ENV`, then `false`.
*/
export function _getDefaultDebugValue(logDebugValue: unknown, nodeEnv?: string): boolean {
if (logDebugValue !== undefined && logDebugValue !== null) {
return Boolean(logDebugValue);
}
if (nodeEnv !== undefined) {
return nodeEnv !== 'production';
}
return false;
}
function getDefaultDebugValue(): boolean {
return _getDefaultDebugValue(log.get('debug'), getNodeEnv());
}
function getNodeEnv(): string | undefined {
const processObject = (
globalThis as typeof globalThis & {
process?: {env?: Record<string, string | undefined>};
}
).process;
if (!processObject?.env) {
return undefined;
}
return processObject.env['NODE_ENV'];
}