diff --git a/docs/api-reference/core/layer.md b/docs/api-reference/core/layer.md index 1e36110fcdc..5775818a870 100644 --- a/docs/api-reference/core/layer.md +++ b/docs/api-reference/core/layer.md @@ -534,6 +534,32 @@ Layers may also include specialized loaders for their own use case, such as imag Find usage examples in the [data loading guide](../../developer-guide/loading-data.md). +#### `memory` ('default' | 'gpu-only', optional) {#memory} + +- Default: `'default'` + +Controls how deck.gl stores generated attribute data in memory. + +- `'default'` retains CPU-side typed arrays alongside GPU buffers. This is the current behavior and enables CPU features such as bounds queries, attribute transitions, and partial updates using cached values. +- `'gpu-only'` uploads generated attributes to GPU buffers and then releases CPU copies. This minimizes CPU memory usage and pooling, but disables features that depend on CPU-side attribute values (e.g. `layer.getBounds()`, attribute transitions, CPU validations) and forces partial updates to regenerate whole attributes or perform GPU-side copies. + +Use `'gpu-only'` when you are feeding data through GPU-heavy pipelines and do not rely on CPU-side attribute inspection. For example: + +```js +import {ScatterplotLayer} from '@deck.gl/layers'; + +const layer = new ScatterplotLayer({ + id: 'points', + data, + memory: 'gpu-only', + getPosition: d => d.position, + getFillColor: [0, 128, 255] +}); +``` + +deck.gl will emit runtime warnings when a CPU-dependent feature is unavailable in this mode. + + #### `fetch` (Function, optional) {#fetch} Called to fetch and parse content from URLs. diff --git a/docs/developer-guide/tips-and-tricks.md b/docs/developer-guide/tips-and-tricks.md index c501101541b..1a8503ade8d 100644 --- a/docs/developer-guide/tips-and-tricks.md +++ b/docs/developer-guide/tips-and-tricks.md @@ -75,3 +75,16 @@ new Deck({ _typedArrayManagerProps: isMobile ? {overAlloc: 1, poolSize: 0} : null }) ``` + +### GPU-only layer attributes + +Layers can opt into storing generated attributes only on the GPU by setting [`memory: 'gpu-only'`](../api-reference/core/layer.md#memory). In this mode, CPU-side typed arrays are released after upload, reducing application memory pressure and avoiding typed array pooling overhead. + +Because CPU copies are not retained, features that depend on CPU-side attribute values are disabled or downgraded: + +- Bounds calculations (`layer.getBounds()` and any culling logic that relies on it) return `null`. +- Attribute transitions are skipped. +- Partial attribute updates regenerate whole attributes or fall back to GPU-side copies, so update ranges are ignored. +- CPU-side validations or transformations that require staging arrays will emit runtime warnings. + +Use this mode when your rendering pipeline is GPU-driven and you do not need CPU inspection of attribute data. Switch back to the default memory behavior if you rely on the features above. diff --git a/docs/whats-new.md b/docs/whats-new.md index edb778e97bd..5805c35d299 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -2,6 +2,10 @@ This page contains highlights of each deck.gl release. Also check our [vis.gl blog](https://medium.com/vis-gl) for news about new releases and features in deck.gl. +## deck.gl v9.3 (upcoming) + +- Layers now accept a `memory: 'gpu-only'` prop to keep generated attributes on the GPU, reducing CPU memory at the cost of CPU-bound features such as bounds queries and attribute transitions. + ## deck.gl v9.2 Target release date: September, 2025 diff --git a/modules/aggregation-layers/src/common/aggregation-layer.ts b/modules/aggregation-layers/src/common/aggregation-layer.ts index 9d4ab4f4ade..d16dfcd615a 100644 --- a/modules/aggregation-layers/src/common/aggregation-layer.ts +++ b/modules/aggregation-layers/src/common/aggregation-layer.ts @@ -101,7 +101,8 @@ export default abstract class AggregationLayer< _getAttributeManager() { return new AttributeManager(this.context.device, { id: this.props.id, - stats: this.context.stats + stats: this.context.stats, + memory: this.props.memory }); } } diff --git a/modules/aggregation-layers/src/heatmap-layer/aggregation-layer.ts b/modules/aggregation-layers/src/heatmap-layer/aggregation-layer.ts index fbfe2b259ab..9c7b4476069 100644 --- a/modules/aggregation-layers/src/heatmap-layer/aggregation-layer.ts +++ b/modules/aggregation-layers/src/heatmap-layer/aggregation-layer.ts @@ -159,7 +159,8 @@ export default abstract class AggregationLayer< _getAttributeManager() { return new AttributeManager(this.context.device, { id: this.props.id, - stats: this.context.stats + stats: this.context.stats, + memory: this.props.memory }); } } diff --git a/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.ts b/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.ts index 3daeca2970d..e4d86a3235c 100644 --- a/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.ts +++ b/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.ts @@ -329,7 +329,8 @@ export default class HeatmapLayer< _getAttributeManager() { return new AttributeManager(this.context.device, { id: this.props.id, - stats: this.context.stats + stats: this.context.stats, + memory: this.props.memory }); } diff --git a/modules/core/src/index.ts b/modules/core/src/index.ts index ef4463aab19..2e6d3f4f178 100644 --- a/modules/core/src/index.ts +++ b/modules/core/src/index.ts @@ -120,7 +120,8 @@ export type { Position, Color, TextureSource, - Material + Material, + MemoryUsage } from './types/layer-props'; export type {DrawLayerParameters, FilterContext} from './passes/layers-pass'; export type {PickingInfo, GetPickingInfoParams} from './lib/picking/pick-info'; diff --git a/modules/core/src/lib/attribute/attribute-manager.ts b/modules/core/src/lib/attribute/attribute-manager.ts index bd471597478..b089f4d1c92 100644 --- a/modules/core/src/lib/attribute/attribute-manager.ts +++ b/modules/core/src/lib/attribute/attribute-manager.ts @@ -15,6 +15,7 @@ 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'; +import type {MemoryUsage} from '../../types/layer-props'; const TRACE_INVALIDATE = 'attributeManager.invalidate'; const TRACE_UPDATE_START = 'attributeManager.updateStart'; @@ -52,26 +53,32 @@ export default class AttributeManager { attributes: Record; updateTriggers: {[name: string]: string[]}; needsRedraw: string | boolean; + memory: MemoryUsage; userData: any; private stats?: Stats; private attributeTransitionManager: AttributeTransitionManager; private mergeBoundsMemoized: any = memoize(mergeBounds); + private transitionsWarningIssued: boolean = false; + private boundsWarningIssued: boolean = false; constructor( device: Device, { id = 'attribute-manager', stats, - timeline + timeline, + memory = 'default' }: { id?: string; stats?: Stats; timeline?: Timeline; + memory?: MemoryUsage; } = {} ) { this.id = id; this.device = device; + this.memory = memory; this.attributes = {}; @@ -237,11 +244,20 @@ export default class AttributeManager { this.stats.get('Update Attributes').timeEnd(); } - this.attributeTransitionManager.update({ - attributes: this.attributes, - numInstances, - transitions - }); + const hasTransitions = transitions && Object.keys(transitions).length > 0; + + if (this.memory !== 'gpu-only') { + this.attributeTransitionManager.update({ + attributes: this.attributes, + numInstances, + transitions + }); + } else if (!this.transitionsWarningIssued && hasTransitions) { + this.transitionsWarningIssued = true; + log.warn( + `${this.id}: attribute transitions are skipped because memory="gpu-only" does not keep CPU copies.` + )(); + } } // Update attribute transition to the current timestamp @@ -259,14 +275,35 @@ export default class AttributeManager { * @return {Object} attributes - descriptors */ getAttributes(): {[id: string]: Attribute} { - return {...this.attributes, ...this.attributeTransitionManager.getAttributes()}; + const transitionAttributes = + this.memory === 'gpu-only' ? {} : this.attributeTransitionManager.getAttributes(); + return {...this.attributes, ...transitionAttributes}; } /** * Computes the spatial bounds of a given set of attributes */ getBounds(attributeNames: string[]) { - const bounds = attributeNames.map(attributeName => this.attributes[attributeName]?.getBounds()); + const bounds = attributeNames.map(attributeName => { + const attribute = this.attributes[attributeName]; + if (!attribute) { + return null; + } + const attributeBounds = attribute.getBounds(); + if ( + !attributeBounds && + this.memory === 'gpu-only' && + !attribute.isConstant && + !attribute.value && + !this.boundsWarningIssued + ) { + this.boundsWarningIssued = true; + log.warn( + `${this.id}: attribute bounds are unavailable in "gpu-only" memory mode. Features that rely on CPU-stored attribute values will be disabled.` + )(); + } + return attributeBounds; + }); return this.mergeBoundsMemoized(bounds); } @@ -278,9 +315,10 @@ export default class AttributeManager { getChangedAttributes(opts: {clearChangedFlags?: boolean} = {clearChangedFlags: false}): { [id: string]: Attribute; } { - const {attributes, attributeTransitionManager} = this; + const {attributes, attributeTransitionManager, memory} = this; - const changedAttributes = {...attributeTransitionManager.getAttributes()}; + const changedAttributes = + memory === 'gpu-only' ? {} : {...attributeTransitionManager.getAttributes()}; for (const attributeName in attributes) { const attribute = attributes[attributeName]; @@ -321,7 +359,8 @@ export default class AttributeManager { ...attribute, id: attributeName, size: (attribute.isIndexed && 1) || attribute.size || 1, - ...overrideOptions + ...overrideOptions, + memory: this.memory }; // Initialize the attribute descriptor, with WebGL and metadata fields diff --git a/modules/core/src/lib/attribute/attribute.ts b/modules/core/src/lib/attribute/attribute.ts index 8e73919cb38..7eff3846588 100644 --- a/modules/core/src/lib/attribute/attribute.ts +++ b/modules/core/src/lib/attribute/attribute.ts @@ -15,6 +15,7 @@ import {fillArray} from '../../utils/flatten'; import * as range from '../../utils/range'; import {bufferLayoutEqual} from './gl-utils'; import {normalizeTransitionSettings, TransitionSettings} from './transition-settings'; +import log from '../../utils/log'; import type {Device, Buffer, BufferLayout} from '@luma.gl/core'; import type {NumericArray, TypedArray} from '../../types/types'; @@ -156,7 +157,7 @@ export default class Attribute extends DataColumn; export type LogicalDataType = DataType | 'float64'; @@ -95,6 +96,7 @@ export type DataColumnOptions = Options & id?: string; vertexOffset?: number; fp64?: boolean; + memory?: MemoryUsage; /** Vertex data type. * @default 'float32' */ @@ -128,6 +130,7 @@ export default class DataColumn { device: Device; id: string; size: number; + memory: MemoryUsage; settings: DataColumnSettings; value: NumericArray | null; doublePrecision: boolean; @@ -140,6 +143,7 @@ export default class DataColumn { this.device = device; this.id = opts.id || ''; this.size = opts.size || 1; + this.memory = opts.memory || 'default'; const logicalType = opts.logicalType || opts.type; const doublePrecision = logicalType === 'float64'; @@ -224,7 +228,11 @@ export default class DataColumn { this._buffer.delete(); this._buffer = null; } - typedArrayManager.release(this.state.allocatedValue); + if (this.memory === 'default') { + typedArrayManager.release(this.state.allocatedValue); + } + this.state.allocatedValue = null; + this.value = null; } getBuffer(): Buffer | null { @@ -240,7 +248,11 @@ export default class DataColumn { ): Record { const result: Record = {}; if (this.state.constant) { - const value = this.value as TypedArray; + const value = this.value as TypedArray | null; + if (!value) { + result[attributeName] = null; + return result; + } if (options) { const shaderAttributeDef = resolveShaderAttribute(this.getAccessor(), options); const offset = shaderAttributeDef.offset / value.BYTES_PER_ELEMENT; @@ -465,6 +477,9 @@ export default class DataColumn { this.state.bounds = null; // clear cached bounds const value = this.value as TypedArray; + if (!value) { + return; + } const {startOffset = 0, endOffset} = opts; this.buffer.write( this.doublePrecision && value instanceof Float64Array @@ -482,12 +497,7 @@ export default class DataColumn { const {state} = this; const oldValue = state.allocatedValue; - // Allocate at least one element to ensure a valid buffer - const value = typedArrayManager.allocate(oldValue, numInstances + 1, { - size: this.size, - type: this.settings.defaultType, - copy - }); + const value = this._allocateCPUValue(numInstances, copy, oldValue); this.value = value; @@ -507,14 +517,44 @@ export default class DataColumn { } } - state.allocatedValue = value; + state.allocatedValue = this.memory === 'default' ? value : null; state.constant = false; state.externalBuffer = null; this.setAccessor(this.settings); return true; } + protected _releaseCPUData() { + if (this.memory === 'gpu-only' && !this.state.constant) { + this.state.allocatedValue = null; + this.value = null; + this.state.bounds = null; + } + } + // PRIVATE HELPER METHODS + private _allocateCPUValue( + numInstances: number, + copy: boolean, + oldValue: TypedArray | null + ): TypedArray { + if (this.memory === 'gpu-only') { + const ArrayType = this.settings.defaultType; + const length = Math.max((numInstances + 1) * this.size, 1); + const value = new ArrayType(length); + if (copy && oldValue) { + value.set(oldValue.subarray(0, Math.min(oldValue.length, value.length))); + } + return value; + } + + return typedArrayManager.allocate(oldValue, numInstances + 1, { + size: this.size, + type: this.settings.defaultType, + copy + }); + } + protected _checkExternalBuffer(opts: {value?: NumericArray; normalized?: boolean}): void { const {value} = opts; if (!ArrayBuffer.isView(value)) { diff --git a/modules/core/src/lib/layer.ts b/modules/core/src/lib/layer.ts index 022cc0efb90..600d2fa732e 100644 --- a/modules/core/src/lib/layer.ts +++ b/modules/core/src/lib/layer.ts @@ -144,6 +144,7 @@ const defaultProps: DefaultProps = { parameters: {type: 'object', value: {}, optional: true, compare: 2}, loadOptions: {type: 'object', value: null, optional: true, ignore: true}, + memory: 'default', transitions: null, extensions: [], loaders: {type: 'array', value: [], optional: true, ignore: true}, @@ -1246,7 +1247,8 @@ export default abstract class Layer extends Component< return new AttributeManager(context.device, { id: this.props.id, stats: context.stats, - timeline: context.timeline + timeline: context.timeline, + memory: this.props.memory }); } diff --git a/modules/core/src/transitions/gpu-interpolation-transition.ts b/modules/core/src/transitions/gpu-interpolation-transition.ts index e1913e770d5..adb670df88b 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -16,6 +16,7 @@ import { getFloat32VertexFormat } from './gpu-transition-utils'; import {GPUTransitionBase} from './gpu-transition'; +import log from '../utils/log'; import type {InterpolationTransitionSettings} from '../lib/attribute/transition-settings'; import type {TypedArray} from '../types/types'; @@ -80,6 +81,12 @@ export default class GPUInterpolationTransition extends GPUTransitionBase attribute.normalizeConstant(getter(value, chunk)); diff --git a/modules/core/src/types/layer-props.ts b/modules/core/src/types/layer-props.ts index bf9f486ac43..4b2a6c216e3 100644 --- a/modules/core/src/types/layer-props.ts +++ b/modules/core/src/types/layer-props.ts @@ -9,6 +9,7 @@ import type {BinaryAttribute} from '../lib/attribute/attribute'; import type {ConstructorOf, NumericArray, TypedArray} from './types'; import type {PickingInfo} from '../lib/picking/pick-info'; import type {MjolnirEvent} from 'mjolnir.js'; +export type MemoryUsage = 'default' | 'gpu-only'; import type {Texture, TextureProps} from '@luma.gl/core'; import type {Buffer, Parameters} from '@luma.gl/core'; @@ -207,6 +208,10 @@ export type LayerProps = { * Options to customize the behavior of loaders */ loadOptions?: any; + /** + * Control whether CPU copies of generated attributes are retained after they are uploaded to GPU buffers. + */ + memory?: MemoryUsage; /** * Callback to calculate the polygonOffset WebGL parameter. */