UNPKG

@deck.gl/core

Version:

deck.gl core library

403 lines (343 loc) 13.2 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import type {Device, RenderPass} from '@luma.gl/core'; import {Timeline} from '@luma.gl/engine'; import type {ShaderAssembler, ShaderModule} from '@luma.gl/shadertools'; import {getShaderAssembler, layerUniforms} from '../shaderlib/index'; import {LIFECYCLE} from '../lifecycle/constants'; import log from '../utils/log'; import debug from '../debug/index'; import {flatten} from '../utils/flatten'; import {Stats} from '@probe.gl/stats'; import ResourceManager from './resource/resource-manager'; import Viewport from '../viewports/viewport'; import type Layer from './layer'; import type CompositeLayer from './composite-layer'; import type Deck from './deck'; const TRACE_SET_LAYERS = 'layerManager.setLayers'; const TRACE_ACTIVATE_VIEWPORT = 'layerManager.activateViewport'; export type LayerContext = { layerManager: LayerManager; resourceManager: ResourceManager; deck?: Deck<any>; device: Device; shaderAssembler: ShaderAssembler; defaultShaderModules: ShaderModule[]; renderPass: RenderPass; stats: Stats; viewport: Viewport; timeline: Timeline; mousePosition: {x: number; y: number} | null; userData: any; onError?: <PropsT extends {}>(error: Error, source: Layer<PropsT>) => void; /** @deprecated Use context.device */ gl: WebGL2RenderingContext; }; export type LayersList = (Layer | undefined | false | null | LayersList)[]; export type LayerManagerProps = { deck?: Deck<any>; stats?: Stats; viewport?: Viewport; timeline?: Timeline; }; export default class LayerManager { layers: Layer[]; context: LayerContext; resourceManager: ResourceManager; private _lastRenderedLayers: LayersList = []; private _needsRedraw: string | false = false; private _needsUpdate: string | false = false; private _nextLayers: LayersList | null = null; private _debug: boolean = false; // This flag is separate from _needsUpdate because it can be set during an update and should trigger another full update private _defaultShaderModulesChanged: boolean = false; /** * @param device * @param param1 */ // eslint-disable-next-line constructor(device: Device, props: LayerManagerProps) { const {deck, stats, viewport, timeline} = props || {}; // Currently deck.gl expects the DeckGL.layers array to be different // whenever React rerenders. If the same layers array is used, the // LayerManager's diffing algorithm will generate a fatal error and // break the rendering. // `this._lastRenderedLayers` stores the UNFILTERED layers sent // down to LayerManager, so that `layers` reference can be compared. // If it's the same across two React render calls, the diffing logic // will be skipped. this.layers = []; this.resourceManager = new ResourceManager({device, protocol: 'deck://'}); this.context = { mousePosition: null, userData: {}, layerManager: this, device, // @ts-expect-error gl: device?.gl, deck, shaderAssembler: getShaderAssembler(device?.info?.shadingLanguage || 'glsl'), defaultShaderModules: [layerUniforms], renderPass: undefined!, stats: stats || new Stats({id: 'deck.gl'}), // Make sure context.viewport is not empty on the first layer initialization viewport: viewport || new Viewport({id: 'DEFAULT-INITIAL-VIEWPORT'}), // Current viewport, exposed to layers for project* function timeline: timeline || new Timeline(), resourceManager: this.resourceManager, onError: undefined }; Object.seal(this); } /** Method to call when the layer manager is not needed anymore. */ finalize() { this.resourceManager.finalize(); // Finalize all layers for (const layer of this.layers) { this._finalizeLayer(layer); } } /** Check if a redraw is needed */ needsRedraw( opts: { /** Reset redraw flags to false after the call */ clearRedrawFlags: boolean; } = {clearRedrawFlags: false} ): string | false { let redraw = this._needsRedraw; if (opts.clearRedrawFlags) { this._needsRedraw = false; } // This layers list doesn't include sublayers, relying on composite layers for (const layer of this.layers) { // Call every layer to clear their flags const layerNeedsRedraw = layer.getNeedsRedraw(opts); redraw = redraw || layerNeedsRedraw; } return redraw; } /** Check if a deep update of all layers is needed */ needsUpdate(): string | false { if (this._nextLayers && this._nextLayers !== this._lastRenderedLayers) { // New layers array may be the same as the old one if `setProps` is called by React return 'layers changed'; } if (this._defaultShaderModulesChanged) { return 'shader modules changed'; } return this._needsUpdate; } /** Layers will be redrawn (in next animation frame) */ setNeedsRedraw(reason: string): void { this._needsRedraw = this._needsRedraw || reason; } /** Layers will be updated deeply (in next animation frame) Potentially regenerating attributes and sub layers */ setNeedsUpdate(reason: string): void { this._needsUpdate = this._needsUpdate || reason; } /** Gets a list of currently rendered layers. Optionally filter by id. */ getLayers({layerIds}: {layerIds?: string[]} = {}): Layer[] { // Filtering by layerId compares beginning of strings, so that sublayers will be included // Dependes on the convention of adding suffixes to the parent's layer name return layerIds ? this.layers.filter(layer => layerIds.find(layerId => layer.id.indexOf(layerId) === 0)) : this.layers; } /** Set props needed for layer rendering and picking. */ setProps(props: any): void { if ('debug' in props) { this._debug = props.debug; } // A way for apps to add data to context that can be accessed in layers if ('userData' in props) { this.context.userData = props.userData; } // New layers will be processed in `updateLayers` in the next update cycle if ('layers' in props) { this._nextLayers = props.layers; } if ('onError' in props) { this.context.onError = props.onError; } } /** Supply a new layer list, initiating sublayer generation and layer matching */ setLayers(newLayers: LayersList, reason?: string): void { debug(TRACE_SET_LAYERS, this, reason, newLayers); this._lastRenderedLayers = newLayers; const flatLayers = flatten(newLayers, Boolean) as Layer[]; for (const layer of flatLayers) { layer.context = this.context; } this._updateLayers(this.layers, flatLayers); } /** Update layers from last cycle if `setNeedsUpdate()` has been called */ updateLayers(): void { // NOTE: For now, even if only some layer has changed, we update all layers // to ensure that layer id maps etc remain consistent even if different // sublayers are rendered const reason = this.needsUpdate(); if (reason) { this.setNeedsRedraw(`updating layers: ${reason}`); // Force a full update this.setLayers(this._nextLayers || this._lastRenderedLayers, reason); } // Updated, clear the backlog this._nextLayers = null; } // // INTERNAL METHODS // /** Make a viewport "current" in layer context, updating viewportChanged flags */ activateViewport = (viewport: Viewport) => { debug(TRACE_ACTIVATE_VIEWPORT, this, viewport); if (viewport) { this.context.viewport = viewport; } }; /** Register a default shader module */ addDefaultShaderModule(module: ShaderModule) { const {defaultShaderModules} = this.context; if (!defaultShaderModules.find(m => m.name === module.name)) { defaultShaderModules.push(module); this._defaultShaderModulesChanged = true; } } /** Deregister a default shader module */ removeDefaultShaderModule(module: ShaderModule) { const {defaultShaderModules} = this.context; const i = defaultShaderModules.findIndex(m => m.name === module.name); if (i >= 0) { defaultShaderModules.splice(i, 1); this._defaultShaderModulesChanged = true; } } private _handleError(stage: string, error: Error, layer: Layer) { layer.raiseError(error, `${stage} of ${layer}`); } // TODO - mark layers with exceptions as bad and remove from rendering cycle? /** Match all layers, checking for caught errors to avoid having an exception in one layer disrupt other layers */ private _updateLayers(oldLayers: Layer[], newLayers: Layer[]): void { // Create old layer map const oldLayerMap: {[layerId: string]: Layer | null} = {}; for (const oldLayer of oldLayers) { if (oldLayerMap[oldLayer.id]) { log.warn(`Multiple old layers with same id ${oldLayer.id}`)(); } else { oldLayerMap[oldLayer.id] = oldLayer; } } if (this._defaultShaderModulesChanged) { for (const layer of oldLayers) { layer.setNeedsUpdate(); layer.setChangeFlags({extensionsChanged: true}); } this._defaultShaderModulesChanged = false; } // Allocate array for generated layers const generatedLayers: Layer[] = []; // Match sublayers this._updateSublayersRecursively(newLayers, oldLayerMap, generatedLayers); // Finalize unmatched layers this._finalizeOldLayers(oldLayerMap); let needsUpdate: string | false = false; for (const layer of generatedLayers) { if (layer.hasUniformTransition()) { needsUpdate = `Uniform transition in ${layer}`; break; } } this._needsUpdate = needsUpdate; this.layers = generatedLayers; } /* eslint-disable complexity,max-statements */ // Note: adds generated layers to `generatedLayers` array parameter private _updateSublayersRecursively( newLayers: Layer[], oldLayerMap: {[layerId: string]: Layer | null}, generatedLayers: Layer[] ) { for (const newLayer of newLayers) { newLayer.context = this.context; // Given a new coming layer, find its matching old layer (if any) const oldLayer = oldLayerMap[newLayer.id]; if (oldLayer === null) { // null, rather than undefined, means this id was originally there log.warn(`Multiple new layers with same id ${newLayer.id}`)(); } // Remove the old layer from candidates, as it has been matched with this layer oldLayerMap[newLayer.id] = null; let sublayers: Layer[] | null = null; // We must not generate exceptions until after layer matching is complete try { if (this._debug && oldLayer !== newLayer) { newLayer.validateProps(); } if (!oldLayer) { this._initializeLayer(newLayer); } else { this._transferLayerState(oldLayer, newLayer); this._updateLayer(newLayer); } generatedLayers.push(newLayer); // Call layer lifecycle method: render sublayers sublayers = newLayer.isComposite ? (newLayer as CompositeLayer).getSubLayers() : null; // End layer lifecycle method: render sublayers } catch (err) { this._handleError('matching', err as Error, newLayer); // Record first exception } if (sublayers) { this._updateSublayersRecursively(sublayers, oldLayerMap, generatedLayers); } } } /* eslint-enable complexity,max-statements */ // Finalize any old layers that were not matched private _finalizeOldLayers(oldLayerMap: {[layerId: string]: Layer | null}): void { for (const layerId in oldLayerMap) { const layer = oldLayerMap[layerId]; if (layer) { this._finalizeLayer(layer); } } } // / EXCEPTION SAFE LAYER ACCESS /** Safely initializes a single layer, calling layer methods */ private _initializeLayer(layer: Layer): void { try { layer._initialize(); layer.lifecycle = LIFECYCLE.INITIALIZED; } catch (err) { this._handleError('initialization', err as Error, layer); // TODO - what should the lifecycle state be here? LIFECYCLE.INITIALIZATION_FAILED? } } /** Transfer state from one layer to a newer version */ private _transferLayerState(oldLayer: Layer, newLayer: Layer): void { newLayer._transferState(oldLayer); newLayer.lifecycle = LIFECYCLE.MATCHED; if (newLayer !== oldLayer) { oldLayer.lifecycle = LIFECYCLE.AWAITING_GC; } } /** Safely updates a single layer, cleaning all flags */ private _updateLayer(layer: Layer): void { try { layer._update(); } catch (err) { this._handleError('update', err as Error, layer); } } /** Safely finalizes a single layer, removing all resources */ private _finalizeLayer(layer: Layer): void { this._needsRedraw = this._needsRedraw || `finalized ${layer}`; layer.lifecycle = LIFECYCLE.AWAITING_FINALIZATION; try { layer._finalize(); layer.lifecycle = LIFECYCLE.FINALIZED; } catch (err) { this._handleError('finalization', err as Error, layer); } } }