Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/api-reference/core/layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps go for 'cpu+gpu' | 'gpu'? 'default' isn't very descriptive

- `'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',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to allow configuration per-attribute? In general transitions are not performed on all the attributes and it seems a shame to have to waste memory just to support a transition for a single attribute.

E.g. a ScatterplotLayer with fixed positions and colors, where we just want to transition the radius

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.
Expand Down
13 changes: 13 additions & 0 deletions docs/developer-guide/tips-and-tricks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we compute the bounds before discarding the CPU data?

- 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.
4 changes: 4 additions & 0 deletions docs/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion modules/aggregation-layers/src/common/aggregation-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}

Expand Down
3 changes: 2 additions & 1 deletion modules/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
61 changes: 50 additions & 11 deletions modules/core/src/lib/attribute/attribute-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,26 +53,32 @@ export default class AttributeManager {
attributes: Record<string, Attribute>;
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 = {};

Expand Down Expand Up @@ -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
Expand All @@ -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);
}

Expand All @@ -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];
Expand Down Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions modules/core/src/lib/attribute/attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -156,7 +157,7 @@ export default class Attribute extends DataColumn<AttributeOptions, AttributeInt
setNeedsUpdate(reason: string = this.id, dataRange?: {startRow?: number; endRow?: number}): void {
this.state.needsUpdate = this.state.needsUpdate || reason;
this.setNeedsRedraw(reason);
if (dataRange) {
if (dataRange && this.memory !== 'gpu-only') {
const {startRow = 0, endRow = Infinity} = dataRange;
this.state.updateRanges = range.add(this.state.updateRanges, [startRow, endRow]);
} else {
Expand Down Expand Up @@ -249,6 +250,10 @@ export default class Attribute extends DataColumn<AttributeOptions, AttributeInt
this.clearNeedsUpdate();
this.setNeedsRedraw();

if (updated) {
this._releaseCPUData();
}

return updated;
}

Expand Down Expand Up @@ -333,10 +338,13 @@ export default class Attribute extends DataColumn<AttributeOptions, AttributeInt
const needsUpdate = settings.transform || startIndices !== this.startIndices;

if (needsUpdate) {
if (ArrayBuffer.isView(buffer)) {
buffer = {value: buffer};
const binaryValue = ArrayBuffer.isView(buffer) ? {value: buffer} : (buffer as BinaryAttribute);
if (this.memory === 'gpu-only' && !ArrayBuffer.isView(binaryValue.value)) {
log.error(
`${this.id}: binary attributes that require transformation need a CPU copy. Provide a typed array or use memory=\"default\".`
)();
return false;
}
const binaryValue = buffer as BinaryAttribute;
assert(ArrayBuffer.isView(binaryValue.value), `invalid ${settings.accessor}`);
const needsNormalize = Boolean(binaryValue.size) && binaryValue.size !== this.size;

Expand Down
Loading
Loading