@luma.gl/core
Version:
The luma.gl core Device API
372 lines • 14 kB
JavaScript
// 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