@luma.gl/core
Version:
The luma.gl core Device API
493 lines • 20.4 kB
JavaScript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { lumaStats } from "../utils/stats-manager.js";
import { log } from "../utils/log.js";
import { uid } from "../utils/uid.js";
import { Buffer } from "./resources/buffer.js";
import { vertexFormatDecoder } from "../shadertypes/vertex-types/vertex-format-decoder.js";
import { textureFormatDecoder } from "../shadertypes/texture-types/texture-format-decoder.js";
import { isExternalImage, getExternalImageSize } from "../shadertypes/image-types/image-types.js";
import { getTextureFormatTable } from "../shadertypes/texture-types/texture-format-table.js";
/** Limits for a device (max supported sizes of resources, max number of bindings etc) */
export class DeviceLimits {
}
function formatErrorLogArguments(context, args) {
const formattedContext = formatErrorLogValue(context);
const formattedArgs = args.map(formatErrorLogValue).filter(arg => arg !== undefined);
return [formattedContext, ...formattedArgs].filter(arg => arg !== undefined);
}
function formatErrorLogValue(value) {
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) {
return ('toString' in value &&
typeof value.toString === 'function' &&
value.toString !== Object.prototype.toString);
}
function looksLikeGPUCompilationMessage(value) {
return 'message' in value && 'type' in value;
}
function formatGPUCompilationMessage(value) {
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 {
features;
disabledFeatures;
constructor(features = [], disabledFeatures) {
this.features = new Set(features);
this.disabledFeatures = disabledFeatures || {};
}
*[Symbol.iterator]() {
yield* this.features;
}
has(feature) {
return !this.disabledFeatures?.[feature] && this.features.has(feature);
}
}
/**
* WebGPU Device/WebGL context abstraction
*/
export class Device {
static defaultProps = {
id: null,
powerPreference: 'high-performance',
failIfMajorPerformanceCaveat: false,
createCanvasContext: undefined,
// WebGL specific
webgl: {},
// Callbacks
// eslint-disable-next-line handle-callback-err
onError: (error, context) => { },
onResize: (context, info) => {
const [width, height] = context.getDevicePixelSize();
log.log(1, `${context} resized => ${width}x${height}px`)();
},
onPositionChange: (context, info) => {
const [left, top] = context.getPosition();
log.log(1, `${context} repositioned => ${left},${top}`)();
},
onVisibilityChange: (context) => log.log(1, `${context} Visibility changed ${context.isVisible}`)(),
onDevicePixelRatioChange: (context, info) => 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]() {
return 'Device';
}
toString() {
return `Device(${this.id})`;
}
/** id of this device, primarily for debugging */
id;
/** A copy of the device props */
props;
/** Available for the application to store data on the device */
userData = {};
/** stats */
statsManager = lumaStats;
/** Internal per-device factory storage */
_factories = {};
/** An abstract timestamp used for change tracking */
timestamp = 0;
/** True if this device has been reused during device creation (app has multiple references) */
_reused = false;
/** Used by other luma.gl modules to store data on the device */
_moduleData = {};
_textureCaps = {};
/** Internal timestamp query set used when GPU timing collection is enabled for this device. */
_debugGPUTimeQuery = null;
constructor(props) {
this.props = { ...Device.defaultProps, ...props };
this.id = this.props.id || uid(this[Symbol.toStringTag].toLowerCase());
}
// TODO - just expose the shadertypes decoders?
getVertexFormatInfo(format) {
return vertexFormatDecoder.getVertexFormatInfo(format);
}
isVertexFormatSupported(format) {
return true;
}
/** Returns information about a texture format, such as data type, channels, bits per channel, compression etc */
getTextureFormatInfo(format) {
return textureFormatDecoder.getInfo(format);
}
/** Determines what operations are supported on a texture format on this particular device (checks against supported device features) */
getTextureFormatCapabilities(format) {
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, height, depth3d = 1) {
const maxSize = Math.max(width, height, depth3d);
return 1 + Math.floor(Math.log2(maxSize));
}
/** Check if data is an external image */
isExternalImage(data) {
return isExternalImage(data);
}
/** Get the size of an external image */
getExternalImageSize(data) {
return getExternalImageSize(data);
}
/** Check if device supports a specific texture format (creation and `nearest` sampling) */
isTextureFormatSupported(format) {
return this.getTextureFormatCapabilities(format).create;
}
/** Check if linear filtering (sampler interpolation) is supported for a specific texture format */
isTextureFormatFilterable(format) {
return this.getTextureFormatCapabilities(format).filter;
}
/** Check if device supports rendering to a framebuffer color attachment of a specific texture format */
isTextureFormatRenderable(format) {
return this.getTextureFormatCapabilities(format).render;
}
/** Check if a specific texture format is GPU compressed */
isTextureFormatCompressed(format) {
return textureFormatDecoder.isCompressed(format);
}
/** Returns the compressed texture formats that can be created and sampled on this device */
getSupportedCompressedTextureFormats() {
const supportedFormats = [];
for (const format of Object.keys(getTextureFormatTable())) {
if (this.isTextureFormatCompressed(format) && this.isTextureFormatSupported(format)) {
supportedFormats.push(format);
}
}
return supportedFormats;
}
// DEBUG METHODS
pushDebugGroup(groupLabel) {
this.commandEncoder.pushDebugGroup(groupLabel);
}
popDebugGroup() {
this.commandEncoder?.popDebugGroup();
}
insertDebugMarker(markerLabel) {
this.commandEncoder?.insertDebugMarker(markerLabel);
}
/**
* Trigger device loss.
* @returns `true` if context loss could actually be triggered.
* @note primarily intended for testing how application reacts to device loss
*/
loseDevice() {
return false;
}
/** A monotonic counter for tracking buffer and texture updates */
incrementTimestamp() {
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, context, ...args) {
// 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() {
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)();
}
}
/** Returns the default / primary canvas context. Throws an error if no canvas context is available (a WebGPU compute device) */
getDefaultCanvasContext() {
if (!this.canvasContext) {
throw new Error('Device has no default CanvasContext. See props.createCanvasContext');
}
return this.canvasContext;
}
/** Create a fence sync object */
createFence() {
throw new Error('createFence() not implemented');
}
/** Create a RenderPass using the default CommandEncoder */
beginRenderPass(props) {
return this.commandEncoder.beginRenderPass(props);
}
/** Create a ComputePass using the default CommandEncoder*/
beginComputePass(props) {
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) {
throw new Error('not implemented');
}
/** Internal helper for creating a shareable WebGL render-pipeline implementation. */
_createSharedRenderPipelineWebGL(_props) {
throw new Error('_createSharedRenderPipelineWebGL() not implemented');
}
/** Internal WebGPU-only helper for retrieving the native bind-group layout for a pipeline group. */
_createBindGroupLayoutWebGPU(_pipeline, _group) {
throw new Error('_createBindGroupLayoutWebGPU() not implemented');
}
/** Internal WebGPU-only helper for creating a native bind group. */
_createBindGroupWebGPU(_bindGroupLayout, _shaderLayout, _bindings, _group, _label) {
throw new Error('_createBindGroupWebGPU() not implemented');
}
/**
* Internal helper that returns `true` when timestamp-query GPU timing should be
* collected for this device.
*/
_supportsDebugGPUTime() {
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 = 256) {
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() {
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() {
return this._debugGPUTimeQuery !== null;
}
// DEPRECATED METHODS
/** @deprecated Use getDefaultCanvasContext() */
getCanvasContext() {
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, options) {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use command encoder */
readPixelsToBufferWebGL(source, options) {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
setParametersWebGL(parameters) {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
getParametersWebGL(parameters) {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use WebGPU parameters (pipeline) */
withParametersWebGL(parameters, func) {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use clear arguments in RenderPass */
clearWebGL(options) {
throw new Error('not implemented');
}
/** @deprecated - will be removed - should use for debugging only */
resetWebGL() {
throw new Error('not implemented');
}
// INTERNAL LUMA.GL METHODS
getModuleData(moduleName) {
this._moduleData[moduleName] ||= {};
return this._moduleData[moduleName];
}
// INTERNAL HELPERS
// IMPLEMENTATION
/** Helper to get the canvas context props */
static _getCanvasContextProps(props) {
return props.createCanvasContext === true ? {} : props.createCanvasContext;
}
_getDeviceTextureFormatCapabilities(format) {
const genericCapabilities = textureFormatDecoder.getCapabilities(format);
// Check standard features
const checkFeature = (feature) => (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)
};
}
/** Subclasses use this to support .createBuffer() overloads */
_normalizeBufferProps(props) {
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, nodeEnv) {
if (logDebugValue !== undefined && logDebugValue !== null) {
return Boolean(logDebugValue);
}
if (nodeEnv !== undefined) {
return nodeEnv !== 'production';
}
return false;
}
function getDefaultDebugValue() {
return _getDefaultDebugValue(log.get('debug'), getNodeEnv());
}
function getNodeEnv() {
const processObject = globalThis.process;
if (!processObject?.env) {
return undefined;
}
return processObject.env['NODE_ENV'];
}
//# sourceMappingURL=device.js.map