UNPKG

@deck.gl/core

Version:

deck.gl core library

264 lines 10.8 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors /* eslint-disable guard-for-in */ import Attribute from "./attribute.js"; import log from "../../utils/log.js"; import memoize from "../../utils/memoize.js"; import { mergeBounds } from "../../utils/math-utils.js"; import debug from "../../debug/index.js"; import AttributeTransitionManager from "./attribute-transition-manager.js"; 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 { constructor(device, { id = 'attribute-manager', stats, timeline } = {}) { this.mergeBoundsMemoized = memoize(mergeBounds); 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: 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) { this._add(attributes); } // Adds attributes addInstanced(attributes) { 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) { 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, dataRange) { const invalidatedAttributes = this._invalidateTrigger(triggerName, dataRange); // For performance tuning debug(TRACE_INVALIDATE, this, triggerName, invalidatedAttributes); } invalidateAll(dataRange) { 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 = {} }) { // 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(context, 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() { return { ...this.attributes, ...this.attributeTransitionManager.getAttributes() }; } /** * Computes the spatial bounds of a given set of attributes */ getBounds(attributeNames) { 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: false }) { 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) { return Object.values(this.getAttributes()).map(attribute => attribute.getBufferLayout(modelInfo)); } // PRIVATE METHODS /** Register new attributes */ _add( /** A map from attribute name to attribute descriptors */ attributes, /** Additional attribute settings to pass to all attributes */ overrideOptions) { for (const attributeName in attributes) { const attribute = attributes[attributeName]; const props = { ...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 _mapUpdateTriggersToAttributes() { const triggers = {}; 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; } _invalidateTrigger(triggerName, dataRange) { 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; } _updateAttribute(opts) { 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(opts.context, 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); } } } //# sourceMappingURL=attribute-manager.js.map