UNPKG

@deck.gl/core

Version:

deck.gl core library

399 lines (347 loc) 12.5 kB
// 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); } } }