UNPKG

@luma.gl/core

Version:

The luma.gl core Device API

493 lines 20.4 kB
// 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