@deck.gl/core
Version:
deck.gl core library
403 lines (343 loc) • 13.2 kB
text/typescript
// 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);
}
}
}