@deck.gl/core
Version:
deck.gl core library
399 lines (347 loc) • 12.5 kB
text/typescript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
/* eslint-disable guard-for-in */
import Attribute, {AttributeOptions} from './attribute';
import log from '../../utils/log';
import memoize from '../../utils/memoize';
import {mergeBounds} from '../../utils/math-utils';
import debug from '../../debug/index';
import {NumericArray} from '../../types/types';
import AttributeTransitionManager from './attribute-transition-manager';
import type {Device, BufferLayout} from '@luma.gl/core';
import type {Stats} from '@probe.gl/stats';
import type {Timeline} from '@luma.gl/engine';
const TRACE_INVALIDATE = 'attributeManager.invalidate';
const TRACE_UPDATE_START = 'attributeManager.updateStart';
const TRACE_UPDATE_END = 'attributeManager.updateEnd';
const TRACE_ATTRIBUTE_UPDATE_START = 'attribute.updateStart';
const TRACE_ATTRIBUTE_ALLOCATE = 'attribute.allocate';
const TRACE_ATTRIBUTE_UPDATE_END = 'attribute.updateEnd';
export default class AttributeManager {
/**
* @classdesc
* Automated attribute generation and management. Suitable when a set of
* vertex shader attributes are generated by iteration over a data array,
* and updates to these attributes are needed either when the data itself
* changes, or when other data relevant to the calculations change.
*
* - First the application registers descriptions of its dynamic vertex
* attributes using AttributeManager.add().
* - Then, when any change that affects attributes is detected by the
* application, the app will call AttributeManager.invalidate().
* - Finally before it renders, it calls AttributeManager.update() to
* ensure that attributes are automatically rebuilt if anything has been
* invalidated.
*
* The application provided update functions describe how attributes
* should be updated from a data array and are expected to traverse
* that data array (or iterable) and fill in the attribute's typed array.
*
* Note that the attribute manager intentionally does not do advanced
* change detection, but instead makes it easy to build such detection
* by offering the ability to "invalidate" each attribute separately.
*/
id: string;
device: Device;
attributes: Record<string, Attribute>;
updateTriggers: {[name: string]: string[]};
needsRedraw: string | boolean;
userData: any;
private stats?: Stats;
private attributeTransitionManager: AttributeTransitionManager;
private mergeBoundsMemoized: any = memoize(mergeBounds);
constructor(
device: Device,
{
id = 'attribute-manager',
stats,
timeline
}: {
id?: string;
stats?: Stats;
timeline?: Timeline;
} = {}
) {
this.id = id;
this.device = device;
this.attributes = {};
this.updateTriggers = {};
this.needsRedraw = true;
this.userData = {};
this.stats = stats;
this.attributeTransitionManager = new AttributeTransitionManager(device, {
id: `${id}-transitions`,
timeline
});
// For debugging sanity, prevent uninitialized members
Object.seal(this);
}
finalize() {
for (const attributeName in this.attributes) {
this.attributes[attributeName].delete();
}
this.attributeTransitionManager.finalize();
}
// Returns the redraw flag, optionally clearing it.
// Redraw flag will be set if any attributes attributes changed since
// flag was last cleared.
//
// @param {String} [clearRedrawFlags=false] - whether to clear the flag
// @return {false|String} - reason a redraw is needed.
getNeedsRedraw(opts: {clearRedrawFlags?: boolean} = {clearRedrawFlags: false}): string | false {
const redraw = this.needsRedraw;
this.needsRedraw = this.needsRedraw && !opts.clearRedrawFlags;
return redraw && this.id;
}
// Sets the redraw flag.
// @param {Boolean} redraw=true
setNeedsRedraw() {
this.needsRedraw = true;
}
// Adds attributes
add(attributes: {[id: string]: AttributeOptions}) {
this._add(attributes);
}
// Adds attributes
addInstanced(attributes: {[id: string]: AttributeOptions}) {
this._add(attributes, {stepMode: 'instance'});
}
/**
* Removes attributes
* Takes an array of attribute names and delete them from
* the attribute map if they exists
*
* @example
* attributeManager.remove(['position']);
*
* @param {Object} attributeNameArray - attribute name array (see above)
*/
remove(attributeNameArray: string[]) {
for (const name of attributeNameArray) {
if (this.attributes[name] !== undefined) {
this.attributes[name].delete();
delete this.attributes[name];
}
}
}
// Marks an attribute for update
invalidate(triggerName: string, dataRange?: {startRow?: number; endRow?: number}) {
const invalidatedAttributes = this._invalidateTrigger(triggerName, dataRange);
// For performance tuning
debug(TRACE_INVALIDATE, this, triggerName, invalidatedAttributes);
}
invalidateAll(dataRange?: {startRow?: number; endRow?: number}) {
for (const attributeName in this.attributes) {
this.attributes[attributeName].setNeedsUpdate(attributeName, dataRange);
}
// For performance tuning
debug(TRACE_INVALIDATE, this, 'all');
}
// Ensure all attribute buffers are updated from props or data.
// eslint-disable-next-line complexity
update({
data,
numInstances,
startIndices = null,
transitions,
props = {},
buffers = {},
context = {}
}: {
data: any;
numInstances: number;
startIndices?: NumericArray | null;
transitions: any;
props: any;
buffers: any;
context: any;
}) {
// keep track of whether some attributes are updated
let updated = false;
debug(TRACE_UPDATE_START, this);
if (this.stats) {
this.stats.get('Update Attributes').timeStart();
}
for (const attributeName in this.attributes) {
const attribute = this.attributes[attributeName];
const accessorName = attribute.settings.accessor;
attribute.startIndices = startIndices;
attribute.numInstances = numInstances;
if (props[attributeName]) {
log.removed(`props.${attributeName}`, `data.attributes.${attributeName}`)();
}
if (attribute.setExternalBuffer(buffers[attributeName])) {
// Step 1: try update attribute directly from external buffers
} else if (
attribute.setBinaryValue(
typeof accessorName === 'string' ? buffers[accessorName] : undefined,
data.startIndices
)
) {
// Step 2: try set packed value from external typed array
} else if (
typeof accessorName === 'string' &&
!buffers[accessorName] &&
attribute.setConstantValue(props[accessorName])
) {
// Step 3: try set constant value from props
// Note: if buffers[accessorName] is supplied, ignore props[accessorName]
// This may happen when setBinaryValue falls through to use the auto updater
} else if (attribute.needsUpdate()) {
// Step 4: update via updater callback
updated = true;
this._updateAttribute({
attribute,
numInstances,
data,
props,
context
});
}
this.needsRedraw = this.needsRedraw || attribute.needsRedraw();
}
if (updated) {
// Only initiate alloc/update (and logging) if actually needed
debug(TRACE_UPDATE_END, this, numInstances);
}
if (this.stats) {
this.stats.get('Update Attributes').timeEnd();
}
this.attributeTransitionManager.update({
attributes: this.attributes,
numInstances,
transitions
});
}
// Update attribute transition to the current timestamp
// Returns `true` if any transition is in progress
updateTransition() {
const {attributeTransitionManager} = this;
const transitionUpdated = attributeTransitionManager.run();
this.needsRedraw = this.needsRedraw || transitionUpdated;
return transitionUpdated;
}
/**
* Returns all attribute descriptors
* Note: Format matches luma.gl Model/Program.setAttributes()
* @return {Object} attributes - descriptors
*/
getAttributes(): {[id: string]: Attribute} {
return {...this.attributes, ...this.attributeTransitionManager.getAttributes()};
}
/**
* Computes the spatial bounds of a given set of attributes
*/
getBounds(attributeNames: string[]) {
const bounds = attributeNames.map(attributeName => this.attributes[attributeName]?.getBounds());
return this.mergeBoundsMemoized(bounds);
}
/**
* Returns changed attribute descriptors
* This indicates which WebGLBuffers need to be updated
* @return {Object} attributes - descriptors
*/
getChangedAttributes(opts: {clearChangedFlags?: boolean} = {clearChangedFlags: false}): {
[id: string]: Attribute;
} {
const {attributes, attributeTransitionManager} = this;
const changedAttributes = {...attributeTransitionManager.getAttributes()};
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
if (attribute.needsRedraw(opts) && !attributeTransitionManager.hasAttribute(attributeName)) {
changedAttributes[attributeName] = attribute;
}
}
return changedAttributes;
}
/** Generate WebGPU-style buffer layout descriptors from all attributes */
getBufferLayouts(
/** A luma.gl Model-shaped object that supplies additional hint to attribute resolution */
modelInfo?: {
/** Whether the model is instanced */
isInstanced?: boolean;
}
): BufferLayout[] {
return Object.values(this.getAttributes()).map(attribute =>
attribute.getBufferLayout(modelInfo)
);
}
// PRIVATE METHODS
/** Register new attributes */
private _add(
/** A map from attribute name to attribute descriptors */
attributes: {[id: string]: AttributeOptions},
/** Additional attribute settings to pass to all attributes */
overrideOptions?: Partial<AttributeOptions>
) {
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
const props: AttributeOptions = {
...attribute,
id: attributeName,
size: (attribute.isIndexed && 1) || attribute.size || 1,
...overrideOptions
};
// Initialize the attribute descriptor, with WebGL and metadata fields
this.attributes[attributeName] = new Attribute(this.device, props);
}
this._mapUpdateTriggersToAttributes();
}
// build updateTrigger name to attribute name mapping
private _mapUpdateTriggersToAttributes() {
const triggers: {[name: string]: string[]} = {};
for (const attributeName in this.attributes) {
const attribute = this.attributes[attributeName];
attribute.getUpdateTriggers().forEach(triggerName => {
if (!triggers[triggerName]) {
triggers[triggerName] = [];
}
triggers[triggerName].push(attributeName);
});
}
this.updateTriggers = triggers;
}
private _invalidateTrigger(
triggerName: string,
dataRange?: {startRow?: number; endRow?: number}
): string[] {
const {attributes, updateTriggers} = this;
const invalidatedAttributes = updateTriggers[triggerName];
if (invalidatedAttributes) {
invalidatedAttributes.forEach(name => {
const attribute = attributes[name];
if (attribute) {
attribute.setNeedsUpdate(attribute.id, dataRange);
}
});
}
return invalidatedAttributes;
}
private _updateAttribute(opts: {
attribute: Attribute;
numInstances: number;
data: any;
props: any;
context: any;
}) {
const {attribute, numInstances} = opts;
debug(TRACE_ATTRIBUTE_UPDATE_START, attribute);
if (attribute.constant) {
// The attribute is flagged as constant outside of an update cycle
// Skip allocation and updater call
// @ts-ignore value can be set to an array by user but always cast to typed array during attribute update
attribute.setConstantValue(attribute.value);
return;
}
if (attribute.allocate(numInstances)) {
debug(TRACE_ATTRIBUTE_ALLOCATE, attribute, numInstances);
}
// Calls update on any buffers that need update
const updated = attribute.updateBuffer(opts);
if (updated) {
this.needsRedraw = true;
debug(TRACE_ATTRIBUTE_UPDATE_END, attribute, numInstances);
}
}
}