UNPKG

@luma.gl/core

Version:

The luma.gl core Device API

372 lines 14 kB
// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { uid } from "../../utils/uid.js"; const CPU_HOTSPOT_PROFILER_MODULE = 'cpu-hotspot-profiler'; const RESOURCE_COUNTS_STATS = 'GPU Resource Counts'; const LEGACY_RESOURCE_COUNTS_STATS = 'Resource Counts'; const GPU_TIME_AND_MEMORY_STATS = 'GPU Time and Memory'; const BASE_RESOURCE_COUNT_ORDER = [ 'Resources', 'Buffers', 'Textures', 'Samplers', 'TextureViews', 'Framebuffers', 'QuerySets', 'Shaders', 'RenderPipelines', 'ComputePipelines', 'PipelineLayouts', 'VertexArrays', 'RenderPasss', 'ComputePasss', 'CommandEncoders', 'CommandBuffers' ]; const WEBGL_RESOURCE_COUNT_ORDER = [ 'Resources', 'Buffers', 'Textures', 'Samplers', 'TextureViews', 'Framebuffers', 'QuerySets', 'Shaders', 'RenderPipelines', 'SharedRenderPipelines', 'ComputePipelines', 'PipelineLayouts', 'VertexArrays', 'RenderPasss', 'ComputePasss', 'CommandEncoders', 'CommandBuffers' ]; const BASE_RESOURCE_COUNT_STAT_ORDER = BASE_RESOURCE_COUNT_ORDER.flatMap(resourceType => [ `${resourceType} Created`, `${resourceType} Active` ]); const WEBGL_RESOURCE_COUNT_STAT_ORDER = WEBGL_RESOURCE_COUNT_ORDER.flatMap(resourceType => [ `${resourceType} Created`, `${resourceType} Active` ]); const ORDERED_STATS_CACHE = new WeakMap(); const ORDERED_STAT_NAME_SET_CACHE = new WeakMap(); /** * Base class for GPU (WebGPU/WebGL) Resources */ export class Resource { /** Default properties for resource */ static defaultProps = { id: 'undefined', handle: undefined, userData: undefined }; toString() { return `${this[Symbol.toStringTag] || this.constructor.name}:"${this.id}"`; } /** props.id, for debugging. */ id; /** The props that this resource was created with */ props; /** User data object, reserved for the application */ userData = {}; /** The device that this resource is associated with - TODO can we remove this dup? */ _device; /** Whether this resource has been destroyed */ destroyed = false; /** For resources that allocate GPU memory */ allocatedBytes = 0; /** Stats bucket currently holding the tracked allocation */ allocatedBytesName = null; /** Attached resources will be destroyed when this resource is destroyed. Tracks auto-created "sub" resources. */ _attachedResources = new Set(); /** * Create a new Resource. Called from Subclass */ constructor(device, props, defaultProps) { if (!device) { throw new Error('no device'); } this._device = device; this.props = selectivelyMerge(props, defaultProps); const id = this.props.id !== 'undefined' ? this.props.id : uid(this[Symbol.toStringTag]); this.props.id = id; this.id = id; this.userData = this.props.userData || {}; this.addStats(); } /** * destroy can be called on any resource to release it before it is garbage collected. */ destroy() { if (this.destroyed) { return; } this.destroyResource(); } /** @deprecated Use destroy() */ delete() { this.destroy(); return this; } /** * Combines a map of user props and default props, only including props from defaultProps * @returns returns a map of overridden default props */ getProps() { return this.props; } // ATTACHED RESOURCES /** * Attaches a resource. Attached resources are auto destroyed when this resource is destroyed * Called automatically when sub resources are auto created but can be called by application */ attachResource(resource) { this._attachedResources.add(resource); } /** * Detach an attached resource. The resource will no longer be auto-destroyed when this resource is destroyed. */ detachResource(resource) { this._attachedResources.delete(resource); } /** * Destroys a resource (only if owned), and removes from the owned (auto-destroy) list for this resource. */ destroyAttachedResource(resource) { if (this._attachedResources.delete(resource)) { resource.destroy(); } } /** Destroy all owned resources. Make sure the resources are no longer needed before calling. */ destroyAttachedResources() { for (const resource of this._attachedResources) { resource.destroy(); } // don't remove while we are iterating this._attachedResources = new Set(); } // PROTECTED METHODS /** Perform all destroy steps. Can be called by derived resources when overriding destroy() */ destroyResource() { if (this.destroyed) { return; } this.destroyAttachedResources(); this.removeStats(); this.destroyed = true; } /** Called by .destroy() to track object destruction. Subclass must call if overriding destroy() */ removeStats() { const profiler = getCpuHotspotProfiler(this._device); const startTime = profiler ? getTimestamp() : 0; const statsObjects = [ this._device.statsManager.getStats(RESOURCE_COUNTS_STATS), this._device.statsManager.getStats(LEGACY_RESOURCE_COUNTS_STATS) ]; const orderedStatNames = getResourceCountStatOrder(this._device); for (const stats of statsObjects) { initializeStats(stats, orderedStatNames); } const name = this.getStatsName(); for (const stats of statsObjects) { stats.get('Resources Active').decrementCount(); stats.get(`${name}s Active`).decrementCount(); } if (profiler) { profiler.statsBookkeepingCalls = (profiler.statsBookkeepingCalls || 0) + 1; profiler.statsBookkeepingTimeMs = (profiler.statsBookkeepingTimeMs || 0) + (getTimestamp() - startTime); } } /** Called by subclass to track memory allocations */ trackAllocatedMemory(bytes, name = this.getStatsName()) { const profiler = getCpuHotspotProfiler(this._device); const startTime = profiler ? getTimestamp() : 0; const stats = this._device.statsManager.getStats(GPU_TIME_AND_MEMORY_STATS); if (this.allocatedBytes > 0 && this.allocatedBytesName) { stats.get('GPU Memory').subtractCount(this.allocatedBytes); stats.get(`${this.allocatedBytesName} Memory`).subtractCount(this.allocatedBytes); } stats.get('GPU Memory').addCount(bytes); stats.get(`${name} Memory`).addCount(bytes); if (profiler) { profiler.statsBookkeepingCalls = (profiler.statsBookkeepingCalls || 0) + 1; profiler.statsBookkeepingTimeMs = (profiler.statsBookkeepingTimeMs || 0) + (getTimestamp() - startTime); } this.allocatedBytes = bytes; this.allocatedBytesName = name; } /** Called by subclass to track handle-backed memory allocations separately from owned allocations */ trackReferencedMemory(bytes, name = this.getStatsName()) { this.trackAllocatedMemory(bytes, `Referenced ${name}`); } /** Called by subclass to track memory deallocations */ trackDeallocatedMemory(name = this.getStatsName()) { if (this.allocatedBytes === 0) { this.allocatedBytesName = null; return; } const profiler = getCpuHotspotProfiler(this._device); const startTime = profiler ? getTimestamp() : 0; const stats = this._device.statsManager.getStats(GPU_TIME_AND_MEMORY_STATS); stats.get('GPU Memory').subtractCount(this.allocatedBytes); stats.get(`${this.allocatedBytesName || name} Memory`).subtractCount(this.allocatedBytes); if (profiler) { profiler.statsBookkeepingCalls = (profiler.statsBookkeepingCalls || 0) + 1; profiler.statsBookkeepingTimeMs = (profiler.statsBookkeepingTimeMs || 0) + (getTimestamp() - startTime); } this.allocatedBytes = 0; this.allocatedBytesName = null; } /** Called by subclass to deallocate handle-backed memory tracked via trackReferencedMemory() */ trackDeallocatedReferencedMemory(name = this.getStatsName()) { this.trackDeallocatedMemory(`Referenced ${name}`); } /** Called by resource constructor to track object creation */ addStats() { const name = this.getStatsName(); const profiler = getCpuHotspotProfiler(this._device); const startTime = profiler ? getTimestamp() : 0; const statsObjects = [ this._device.statsManager.getStats(RESOURCE_COUNTS_STATS), this._device.statsManager.getStats(LEGACY_RESOURCE_COUNTS_STATS) ]; const orderedStatNames = getResourceCountStatOrder(this._device); for (const stats of statsObjects) { initializeStats(stats, orderedStatNames); } for (const stats of statsObjects) { stats.get('Resources Created').incrementCount(); stats.get('Resources Active').incrementCount(); stats.get(`${name}s Created`).incrementCount(); stats.get(`${name}s Active`).incrementCount(); } if (profiler) { profiler.statsBookkeepingCalls = (profiler.statsBookkeepingCalls || 0) + 1; profiler.statsBookkeepingTimeMs = (profiler.statsBookkeepingTimeMs || 0) + (getTimestamp() - startTime); } recordTransientCanvasResourceCreate(this._device, name); } /** Canonical resource name used for stats buckets. */ getStatsName() { return getCanonicalResourceName(this); } } /** * Combines a map of user props and default props, only including props from defaultProps * @param props * @param defaultProps * @returns returns a map of overridden default props */ function selectivelyMerge(props, defaultProps) { const mergedProps = { ...defaultProps }; for (const key in props) { if (props[key] !== undefined) { mergedProps[key] = props[key]; } } return mergedProps; } function initializeStats(stats, orderedStatNames) { const statsMap = stats.stats; let addedOrderedStat = false; for (const statName of orderedStatNames) { if (!statsMap[statName]) { stats.get(statName); addedOrderedStat = true; } } const statCount = Object.keys(statsMap).length; const cachedStats = ORDERED_STATS_CACHE.get(stats); if (!addedOrderedStat && cachedStats?.orderedStatNames === orderedStatNames && cachedStats.statCount === statCount) { return; } const reorderedStats = {}; let orderedStatNamesSet = ORDERED_STAT_NAME_SET_CACHE.get(orderedStatNames); if (!orderedStatNamesSet) { orderedStatNamesSet = new Set(orderedStatNames); ORDERED_STAT_NAME_SET_CACHE.set(orderedStatNames, orderedStatNamesSet); } for (const statName of orderedStatNames) { if (statsMap[statName]) { reorderedStats[statName] = statsMap[statName]; } } for (const [statName, stat] of Object.entries(statsMap)) { if (!orderedStatNamesSet.has(statName)) { reorderedStats[statName] = stat; } } for (const statName of Object.keys(statsMap)) { delete statsMap[statName]; } Object.assign(statsMap, reorderedStats); ORDERED_STATS_CACHE.set(stats, { orderedStatNames, statCount }); } function getResourceCountStatOrder(device) { return device.type === 'webgl' ? WEBGL_RESOURCE_COUNT_STAT_ORDER : BASE_RESOURCE_COUNT_STAT_ORDER; } function getCpuHotspotProfiler(device) { const profiler = device.userData[CPU_HOTSPOT_PROFILER_MODULE]; return profiler?.enabled ? profiler : null; } function getTimestamp() { return globalThis.performance?.now?.() ?? Date.now(); } function recordTransientCanvasResourceCreate(device, name) { const profiler = getCpuHotspotProfiler(device); if (!profiler || !profiler.activeDefaultFramebufferAcquireDepth) { return; } profiler.transientCanvasResourceCreates = (profiler.transientCanvasResourceCreates || 0) + 1; switch (name) { case 'Texture': profiler.transientCanvasTextureCreates = (profiler.transientCanvasTextureCreates || 0) + 1; break; case 'TextureView': profiler.transientCanvasTextureViewCreates = (profiler.transientCanvasTextureViewCreates || 0) + 1; break; case 'Sampler': profiler.transientCanvasSamplerCreates = (profiler.transientCanvasSamplerCreates || 0) + 1; break; case 'Framebuffer': profiler.transientCanvasFramebufferCreates = (profiler.transientCanvasFramebufferCreates || 0) + 1; break; default: break; } } function getCanonicalResourceName(resource) { let prototype = Object.getPrototypeOf(resource); while (prototype) { const parentPrototype = Object.getPrototypeOf(prototype); if (!parentPrototype || parentPrototype === Resource.prototype) { return (getPrototypeToStringTag(prototype) || resource[Symbol.toStringTag] || resource.constructor.name); } prototype = parentPrototype; } return resource[Symbol.toStringTag] || resource.constructor.name; } function getPrototypeToStringTag(prototype) { const descriptor = Object.getOwnPropertyDescriptor(prototype, Symbol.toStringTag); if (typeof descriptor?.get === 'function') { return descriptor.get.call(prototype); } if (typeof descriptor?.value === 'string') { return descriptor.value; } return null; } //# sourceMappingURL=resource.js.map