@deck.gl/core
Version:
deck.gl core library
309 lines • 12.7 kB
JavaScript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { Timeline } from '@luma.gl/engine';
import { getShaderAssembler, layerUniforms } from "../shaderlib/index.js";
import { LIFECYCLE } from "../lifecycle/constants.js";
import log from "../utils/log.js";
import debug from "../debug/index.js";
import { flatten } from "../utils/flatten.js";
import { Stats } from '@probe.gl/stats';
import ResourceManager from "./resource/resource-manager.js";
import Viewport from "../viewports/viewport.js";
const TRACE_SET_LAYERS = 'layerManager.setLayers';
const TRACE_ACTIVATE_VIEWPORT = 'layerManager.activateViewport';
export default class LayerManager {
/**
* @param device
* @param param1
*/
// eslint-disable-next-line
constructor(device, props) {
this._lastRenderedLayers = [];
this._needsRedraw = false;
this._needsUpdate = false;
this._nextLayers = null;
this._debug = false;
// This flag is separate from _needsUpdate because it can be set during an update and should trigger another full update
this._defaultShaderModulesChanged = false;
//
// INTERNAL METHODS
//
/** Make a viewport "current" in layer context, updating viewportChanged flags */
this.activateViewport = (viewport) => {
debug(TRACE_ACTIVATE_VIEWPORT, this, viewport);
if (viewport) {
this.context.viewport = viewport;
}
};
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 = { clearRedrawFlags: 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() {
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) {
this._needsRedraw = this._needsRedraw || reason;
}
/** Layers will be updated deeply (in next animation frame)
Potentially regenerating attributes and sub layers */
setNeedsUpdate(reason) {
this._needsUpdate = this._needsUpdate || reason;
}
/** Gets a list of currently rendered layers. Optionally filter by id. */
getLayers({ layerIds } = {}) {
// 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) {
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, reason) {
debug(TRACE_SET_LAYERS, this, reason, newLayers);
this._lastRenderedLayers = newLayers;
const flatLayers = flatten(newLayers, Boolean);
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() {
// 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;
}
/** Register a default shader module */
addDefaultShaderModule(module) {
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) {
const { defaultShaderModules } = this.context;
const i = defaultShaderModules.findIndex(m => m.name === module.name);
if (i >= 0) {
defaultShaderModules.splice(i, 1);
this._defaultShaderModulesChanged = true;
}
}
_handleError(stage, error, 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 */
_updateLayers(oldLayers, newLayers) {
// Create old layer map
const oldLayerMap = {};
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 = [];
// Match sublayers
this._updateSublayersRecursively(newLayers, oldLayerMap, generatedLayers);
// Finalize unmatched layers
this._finalizeOldLayers(oldLayerMap);
let needsUpdate = 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
_updateSublayersRecursively(newLayers, oldLayerMap, generatedLayers) {
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 = 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.getSubLayers() : null;
// End layer lifecycle method: render sublayers
}
catch (err) {
this._handleError('matching', err, newLayer); // Record first exception
}
if (sublayers) {
this._updateSublayersRecursively(sublayers, oldLayerMap, generatedLayers);
}
}
}
/* eslint-enable complexity,max-statements */
// Finalize any old layers that were not matched
_finalizeOldLayers(oldLayerMap) {
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 */
_initializeLayer(layer) {
try {
layer._initialize();
layer.lifecycle = LIFECYCLE.INITIALIZED;
}
catch (err) {
this._handleError('initialization', err, layer);
// TODO - what should the lifecycle state be here? LIFECYCLE.INITIALIZATION_FAILED?
}
}
/** Transfer state from one layer to a newer version */
_transferLayerState(oldLayer, newLayer) {
newLayer._transferState(oldLayer);
newLayer.lifecycle = LIFECYCLE.MATCHED;
if (newLayer !== oldLayer) {
oldLayer.lifecycle = LIFECYCLE.AWAITING_GC;
}
}
/** Safely updates a single layer, cleaning all flags */
_updateLayer(layer) {
try {
layer._update();
}
catch (err) {
this._handleError('update', err, layer);
}
}
/** Safely finalizes a single layer, removing all resources */
_finalizeLayer(layer) {
this._needsRedraw = this._needsRedraw || `finalized ${layer}`;
layer.lifecycle = LIFECYCLE.AWAITING_FINALIZATION;
try {
layer._finalize();
layer.lifecycle = LIFECYCLE.FINALIZED;
}
catch (err) {
this._handleError('finalization', err, layer);
}
}
}
//# sourceMappingURL=layer-manager.js.map