From cbe63ef93166abb50ec0815032119f2bdda8bcc0 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Mon, 11 Mar 2024 14:59:05 -0700 Subject: [PATCH 01/12] Update test app --- test/apps/attribute-transition/app.jsx | 220 ++++++++------------ test/apps/attribute-transition/package.json | 2 +- 2 files changed, 93 insertions(+), 129 deletions(-) diff --git a/test/apps/attribute-transition/app.jsx b/test/apps/attribute-transition/app.jsx index ff3db269298..6d215ed952a 100644 --- a/test/apps/attribute-transition/app.jsx +++ b/test/apps/attribute-transition/app.jsx @@ -1,149 +1,113 @@ /* global document console */ /* eslint-disable no-console */ -import React, {Component} from 'react'; +import React, {useMemo, useState, useCallback} from 'react'; import {createRoot} from 'react-dom/client'; -import DeckGL, {COORDINATE_SYSTEM, OrthographicView, ScatterplotLayer, PolygonLayer} from 'deck.gl'; +import DeckGL, {OrthographicView, ScatterplotLayer, PolygonLayer} from 'deck.gl'; import DataGenerator from './data-generator'; -class Root extends Component { - constructor(props) { - super(props); +const initialViewState = { + target: [0, 0, 0], + zoom: 0 +}; - this._dataGenerator = new DataGenerator(); +const interpolationSettings = { + duration: 600 + // onStart: () => console.log('onStart'), + // onEnd: () => console.log('onEnd'), + // onInterrupt: () => console.log('onInterrupt') +}; - this.state = { - transitionType: 'spring', - points: this._dataGenerator.points, - polygons: this._dataGenerator.polygons, - viewState: { - target: [0, 0, 0], - zoom: 0 - } - }; +const springSettings = { + type: 'spring', + stiffness: 0.01, + damping: 0.15 + // onStart: () => console.log('onStart'), + // onEnd: () => console.log('onEnd'), + // onInterrupt: () => console.log('onInterrupt') +}; - this._randomize = this._randomize.bind(this); - this._onChangeTransitionType = this._onChangeTransitionType.bind(this); +const scatterplotTransitionsByType = { + interpolation: { + getPosition: {...interpolationSettings, enter: () => [0, 0]}, + getRadius: {...interpolationSettings, enter: () => [0]}, + getFillColor: {...interpolationSettings, enter: ([r, g, b]) => [r, g, b, 0]} + }, + spring: { + getPosition: {...springSettings, enter: () => [0, 0]}, + getRadius: {...springSettings, enter: () => [0]}, + getFillColor: {...springSettings, enter: ([r, g, b]) => [r, g, b, 0]} } +}; - _randomize() { - this._dataGenerator.randomize(); - this.setState({ - points: this._dataGenerator.points, - polygons: this._dataGenerator.polygons - }); +const polygonTransitionsByType = { + interpolation: { + getPolygon: 600, + getLineColor: {...interpolationSettings, enter: ([r, g, b]) => [r, g, b, 0]}, + getFillColor: {...interpolationSettings, enter: ([r, g, b]) => [r, g, b, 0]}, + getLineWidth: 600 + }, + spring: { + getPolygon: springSettings, + getLineColor: {...springSettings, enter: ([r, g, b]) => [r, g, b, 0]}, + getFillColor: {...springSettings, enter: ([r, g, b]) => [r, g, b, 0]}, + getLineWidth: springSettings } +}; - _onChangeTransitionType({currentTarget}) { - this.setState({ - transitionType: currentTarget.value - }); - } - - render() { - const {points, polygons, viewState} = this.state; - - const interpolationSettings = { - duration: 600, - onStart: () => { - console.log('onStart'); - }, - onEnd: () => { - console.log('onEnd'); - }, - onInterrupt: () => { - console.log('onInterrupt'); - } - }; +function Root() { + const dataGenerator = useMemo(() => new DataGenerator()); + const [points, setPoints] = useState(dataGenerator.points); + const [polygons, setPolygons] = useState(dataGenerator.polygons); + const [transitionType, setTransitionType] = useState('interpolation'); - const springSettings = { - type: 'spring', - stiffness: 0.01, - damping: 0.15, - onStart: () => { - console.log('onStart'); - }, - onEnd: () => { - console.log('onEnd'); - }, - onInterrupt: () => { - console.log('onInterrupt'); - } - }; + const randomize = useCallback(() => { + dataGenerator.randomize(); + setPoints(dataGenerator.points); + setPolygons(dataGenerator.polygons); + }, [dataGenerator]); - const scatterplotTransitionsByType = { - interpolation: { - getPosition: Object.assign({}, interpolationSettings, {enter: () => [0, 0]}), - getRadius: Object.assign({}, interpolationSettings, {enter: () => [0]}), - getFillColor: Object.assign({}, interpolationSettings, {enter: ([r, g, b]) => [r, g, b, 0]}) - }, - spring: { - getPosition: Object.assign({}, springSettings, {enter: () => [0, 0]}), - getRadius: Object.assign({}, springSettings, {enter: () => [0]}), - getFillColor: Object.assign({}, springSettings, {enter: ([r, g, b]) => [r, g, b, 0]}) - } - }; + const onChangeTransitionType = useCallback(({currentTarget}) => { + setTransitionType(currentTarget.value); + }); - const polygonTransitionsByType = { - interpolation: { - getPolygon: 600, - getLineColor: Object.assign({}, interpolationSettings, { - enter: ([r, g, b]) => [r, g, b, 0] - }), - getFillColor: Object.assign({}, interpolationSettings, { - enter: ([r, g, b]) => [r, g, b, 0] - }), - getLineWidth: 600 - }, - spring: { - getPolygon: springSettings, - getLineColor: Object.assign({}, springSettings, {enter: ([r, g, b]) => [r, g, b, 0]}), - getFillColor: Object.assign({}, springSettings, {enter: ([r, g, b]) => [r, g, b, 0]}), - getLineWidth: springSettings - } - }; + const layers = [ + new ScatterplotLayer({ + data: points, + getPosition: d => d.position, + getFillColor: d => d.color, + getRadius: d => d.radius, + transitions: scatterplotTransitionsByType[transitionType] + }), + new PolygonLayer({ + data: polygons, + stroked: true, + filled: true, + getPolygon: d => d.polygon, + getLineColor: d => d.color, + getFillColor: d => [d.color[0], d.color[1], d.color[2], 128], + getLineWidth: d => d.width, + transitions: polygonTransitionsByType[transitionType] + }) + ]; - const layers = [ - new ScatterplotLayer({ - coordinateSystem: COORDINATE_SYSTEM.IDENTITY, - data: points, - getPosition: d => d.position, - getFillColor: d => d.color, - getRadius: d => d.radius, - transitions: scatterplotTransitionsByType[this.state.transitionType] - }), - new PolygonLayer({ - coordinateSystem: COORDINATE_SYSTEM.IDENTITY, - data: polygons, - stroked: true, - filled: true, - getPolygon: d => d.polygon, - getLineColor: d => d.color, - getFillColor: d => [d.color[0], d.color[1], d.color[2], 128], - getLineWidth: d => d.width, - transitions: polygonTransitionsByType[this.state.transitionType] - }) - ]; - - return ( -
- this.setState({viewState: evt.viewState})} - layers={layers} - /> -
- - -
+ return ( +
+ +
+ +
- ); - } +
+ ); } const container = document.body.appendChild(document.createElement('div')); diff --git a/test/apps/attribute-transition/package.json b/test/apps/attribute-transition/package.json index 9f0dc987d70..841f466fd19 100644 --- a/test/apps/attribute-transition/package.json +++ b/test/apps/attribute-transition/package.json @@ -4,7 +4,7 @@ "start-local": "vite --config ../vite.config.local.mjs" }, "dependencies": { - "deck.gl": "^8.4.0", + "deck.gl": "^9.0.0-beta", "react": "^18.0.0", "react-dom": "^18.0.0" }, From 776a3bb29b270ec007b6eb45ce36b60169c8cbb1 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 12:00:08 -0700 Subject: [PATCH 02/12] wip --- .../src/lib/attribute/attribute-manager.ts | 2 +- .../attribute/attribute-transition-manager.ts | 4 +- modules/core/src/lib/attribute/attribute.ts | 5 +- modules/core/src/lib/attribute/data-column.ts | 8 +- .../src/lib/uniform-transition-manager.ts | 2 +- .../gpu-interpolation-transition.ts | 96 ++++++------- .../src/transitions/gpu-spring-transition.ts | 8 +- .../gpu-transition-utils.ts} | 128 ++++++------------ .../core/src/transitions/gpu-transition.ts | 4 +- .../src/transitions/transition-settings.ts | 59 ++++++++ modules/core/src/utils/array-utils.ts | 46 +++++-- 11 files changed, 203 insertions(+), 159 deletions(-) rename modules/core/src/{lib/attribute/attribute-transition-utils.ts => transitions/gpu-transition-utils.ts} (59%) create mode 100644 modules/core/src/transitions/transition-settings.ts diff --git a/modules/core/src/lib/attribute/attribute-manager.ts b/modules/core/src/lib/attribute/attribute-manager.ts index 60d1bf13308..e1c68a66e5b 100644 --- a/modules/core/src/lib/attribute/attribute-manager.ts +++ b/modules/core/src/lib/attribute/attribute-manager.ts @@ -275,7 +275,7 @@ export default class AttributeManager { * @return {Object} attributes - descriptors */ getAttributes(): {[id: string]: Attribute} { - return this.attributes; + return {...this.attributes, ...this.attributeTransitionManager.getAttributes()}; } /** diff --git a/modules/core/src/lib/attribute/attribute-transition-manager.ts b/modules/core/src/lib/attribute/attribute-transition-manager.ts index b15fbec9eaa..1a1cd2cb106 100644 --- a/modules/core/src/lib/attribute/attribute-transition-manager.ts +++ b/modules/core/src/lib/attribute/attribute-transition-manager.ts @@ -6,10 +6,10 @@ import log from '../../utils/log'; import type {Device} from '@luma.gl/core'; import type {Timeline} from '@luma.gl/engine'; -import type GPUTransition from '../../transitions/gpu-transition'; +import type {GPUTransition} from '../../transitions/gpu-transition'; import type {ConstructorOf} from '../../types/types'; import type Attribute from './attribute'; -import type {TransitionSettings} from './attribute-transition-utils'; +import type {TransitionSettings} from '../../transitions/transition-settings'; const TRANSITION_TYPES: Record> = { interpolation: GPUInterpolationTransition, diff --git a/modules/core/src/lib/attribute/attribute.ts b/modules/core/src/lib/attribute/attribute.ts index 8b663199e76..fbf21d2d879 100644 --- a/modules/core/src/lib/attribute/attribute.ts +++ b/modules/core/src/lib/attribute/attribute.ts @@ -10,7 +10,10 @@ import {createIterable, getAccessorFromBuffer} from '../../utils/iterable-utils' import {fillArray} from '../../utils/flatten'; import * as range from '../../utils/range'; import {bufferLayoutEqual} from './gl-utils'; -import {normalizeTransitionSettings, TransitionSettings} from './attribute-transition-utils'; +import { + normalizeTransitionSettings, + TransitionSettings +} from '../../transitions/transition-settings'; import type {Device, Buffer, BufferLayout} from '@luma.gl/core'; import type {NumericArray, TypedArray} from '../../types/types'; diff --git a/modules/core/src/lib/attribute/data-column.ts b/modules/core/src/lib/attribute/data-column.ts index a23c8c39638..69631c18674 100644 --- a/modules/core/src/lib/attribute/data-column.ts +++ b/modules/core/src/lib/attribute/data-column.ts @@ -502,7 +502,6 @@ export default class DataColumn { if (!ArrayBuffer.isView(value)) { throw new Error(`Attribute ${this.id} value is not TypedArray`); } - const ArrayType = this.settings.defaultType; let illegalArrayType = false; if (this.doublePrecision) { @@ -512,9 +511,10 @@ export default class DataColumn { if (illegalArrayType) { throw new Error(`Attribute ${this.id} does not support ${value.constructor.name}`); } - if (!(value instanceof ArrayType) && this.settings.normalized && !('normalized' in opts)) { - log.warn(`Attribute ${this.id} is normalized`)(); - } + // const ArrayType = this.settings.defaultType; + // if (!(value instanceof ArrayType) && this.settings.normalized && !('normalized' in opts)) { + // log.warn(`Attribute ${this.id} is normalized`)(); + // } } // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer diff --git a/modules/core/src/lib/uniform-transition-manager.ts b/modules/core/src/lib/uniform-transition-manager.ts index d32c4472d98..17e062668f0 100644 --- a/modules/core/src/lib/uniform-transition-manager.ts +++ b/modules/core/src/lib/uniform-transition-manager.ts @@ -1,4 +1,4 @@ -import {normalizeTransitionSettings} from './attribute/attribute-transition-utils'; +import {normalizeTransitionSettings} from '../transitions/transition-settings'; import CPUInterpolationTransition from '../transitions/cpu-interpolation-transition'; import CPUSpringTransition from '../transitions/cpu-spring-transition'; import log from '../utils/log'; diff --git a/modules/core/src/transitions/gpu-interpolation-transition.ts b/modules/core/src/transitions/gpu-interpolation-transition.ts index d51ff9bb99e..d6a18c0eba1 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -1,4 +1,4 @@ -import type {Device} from '@luma.gl/core'; +import type {Device, VertexFormat} from '@luma.gl/core'; import {Timeline, BufferTransform} from '@luma.gl/engine'; import {Buffer} from '@luma.gl/core'; import {GL} from '@luma.gl/constants'; @@ -7,14 +7,14 @@ import { getAttributeTypeFromSize, getAttributeBufferLength, cycleBuffers, - InterpolationTransitionSettings, padBuffer, getFloat32VertexFormat -} from '../lib/attribute/attribute-transition-utils'; +} from './gpu-transition-utils'; import Transition from './transition'; -import type {NumericArray} from '../types/types'; -import type GPUTransition from './gpu-transition'; +import type {InterpolationTransitionSettings} from './transition-settings'; +import type {NumericArray, TypedArray} from '../types/types'; +import type {GPUTransition} from './gpu-transition'; export default class GPUInterpolationTransition implements GPUTransition { device: Device; @@ -43,26 +43,20 @@ export default class GPUInterpolationTransition implements GPUTransition { this.attribute = attribute; // this is the attribute we return during the transition - note: if it is a constant // attribute, it will be converted and returned as a regular attribute - // `attribute.userData` is the original options passed when constructing the attribute. + // `attribute.settings` is the original options passed when constructing the attribute. // This ensures that we set the proper `doublePrecision` flag and shader attributes. this.attributeInTransition = new Attribute(device, attribute.settings); - if (ArrayBuffer.isView(attribute.value)) { - this.attributeInTransition.setData(attribute.value); - } + // Placeholder value - necessary for generating the correct buffer layout + this.attributeInTransition.setData( + attribute.value instanceof Float64Array ? new Float64Array(0) : new Float32Array(0) + ); this.currentStartIndices = attribute.startIndices; // storing currentLength because this.buffer may be larger than the actual length we want to use // this is because we only reallocate buffers when they grow, not when they shrink, // due to performance costs this.currentLength = 0; this.transform = getTransform(device, attribute); - const bufferOpts = { - byteLength: attribute.buffer.byteLength, - usage: GL.DYNAMIC_COPY - }; - this.buffers = [ - device.createBuffer(bufferOpts), // from - device.createBuffer(bufferOpts) // current - ]; + this.buffers = [device.createBuffer({byteLength: 0})]; } get inProgress(): boolean { @@ -87,59 +81,67 @@ export default class GPUInterpolationTransition implements GPUTransition { // And the other buffer is now the current buffer. cycleBuffers(buffers); + const toLength = getAttributeBufferLength(attribute, numInstances); const padBufferOpts = { numInstances, attribute, fromLength: this.currentLength, + toLength, fromStartIndices: this.currentStartIndices, getData: transitionSettings.enter }; - for (const [index, buffer] of buffers.entries()) { - const paddedBuffer = padBuffer({buffer, ...padBufferOpts}); - - if (buffer !== paddedBuffer) { - buffer.destroy(); - buffers[index] = paddedBuffer; - - // TODO(v9): While this probably isn't necessary as a user-facing warning, it is helpful - // for debugging buffer allocation during deck.gl v9 development. - console.warn( - `[GPUInterpolationTransition] Replaced buffer ${buffer.id} (${buffer.byteLength} bytes) → ` + - `${paddedBuffer.id} (${paddedBuffer.byteLength} bytes)` - ); - } + const fromBuffer = padBuffer({buffer: buffers[0], ...padBufferOpts}); + if (fromBuffer !== buffers[0]) { + buffers[0].destroy(); + buffers[0] = fromBuffer; + } + if (!buffers[1] || buffers[1].byteLength < fromBuffer.byteLength) { + buffers[1]?.destroy(); + buffers[1] = this.device.createBuffer({ + byteLength: fromBuffer.byteLength, + usage: fromBuffer.usage + }); } this.currentStartIndices = attribute.startIndices; - this.currentLength = getAttributeBufferLength(attribute, numInstances); + this.currentLength = toLength; this.attributeInTransition.setData({ buffer: buffers[1], - // Hack: Float64Array is required for double-precision attributes - // to generate correct shader attributes - value: attribute.value as NumericArray + // Retain placeholder value to generate correct shader layout + value: this.attributeInTransition.value as NumericArray }); this.transition.start(transitionSettings); - this.transform.model.setVertexCount(Math.floor(this.currentLength / attribute.size)); - // TODO(v9): Best way to handle 'constant' attributes? - this.transform.model.setAttributes( - attribute.getBuffer() ? {aFrom: buffers[0], aTo: attribute.getBuffer()!} : {aFrom: buffers[0]} - ); + const {model} = this.transform; + model.setVertexCount(Math.floor(this.currentLength / attribute.size)); + if (attribute.isConstant) { + model.setAttributes({aFrom: buffers[0]}); + model.setConstantAttributes({aTo: attribute.value as TypedArray}); + } else { + model.setAttributes({ + aFrom: buffers[0], + aTo: attribute.getBuffer()! + }); + } this.transform.transformFeedback.setBuffers({vCurrent: buffers[1]}); } update(): boolean { const updated = this.transition.update(); if (updated) { - const {duration, easing} = this.settings as InterpolationTransitionSettings; + const {duration, easing} = this.settings!; const {time} = this.transition; let t = time / duration; if (easing) { t = easing(t); } - this.transform.model.setUniforms({time: t}); + const {model} = this.transform; + model.setUniforms({time: t}); + // TODO - why is this needed? + model.setAttributes({aFrom: this.buffers[0]}); + this.transform.run(); } return updated; @@ -147,9 +149,9 @@ export default class GPUInterpolationTransition implements GPUTransition { cancel(): void { this.transition.cancel(); - this.transform.delete(); + this.transform.destroy(); for (const buffer of this.buffers) { - buffer.delete(); + buffer.destroy(); } this.buffers.length = 0; } @@ -172,12 +174,12 @@ void main(void) { function getTransform(device: Device, attribute: Attribute): BufferTransform { const attributeType = getAttributeTypeFromSize(attribute.size); - const format = getFloat32VertexFormat(attribute.size as 1 | 2 | 3 | 4); return new BufferTransform(device, { + id: `${attribute.id}-transition`, vs, bufferLayout: [ - {name: 'aFrom', format}, - {name: 'aTo', format} + {name: 'aFrom', format: getFloat32VertexFormat(attribute.size)}, + {name: 'aTo', format: attribute.getBufferLayout().attributes![0].format} ], defines: { ATTRIBUTE_TYPE: attributeType diff --git a/modules/core/src/transitions/gpu-spring-transition.ts b/modules/core/src/transitions/gpu-spring-transition.ts index 4bafae7a22a..27ed202fe4f 100644 --- a/modules/core/src/transitions/gpu-spring-transition.ts +++ b/modules/core/src/transitions/gpu-spring-transition.ts @@ -7,12 +7,12 @@ import { getAttributeTypeFromSize, getAttributeBufferLength, getFloat32VertexFormat, - cycleBuffers, - SpringTransitionSettings -} from '../lib/attribute/attribute-transition-utils'; + cycleBuffers +} from './gpu-transition-utils'; import Attribute from '../lib/attribute/attribute'; import Transition from './transition'; +import type {SpringTransitionSettings} from './transition-settings'; import type {Timeline} from '@luma.gl/engine'; import type {BufferTransform as LumaTransform} from '@luma.gl/engine'; import type { @@ -21,7 +21,7 @@ import type { Texture as LumaTexture2D } from '@luma.gl/core'; import type {NumericArray} from '../types/types'; -import type GPUTransition from './gpu-transition'; +import type {GPUTransition} from './gpu-transition'; export default class GPUSpringTransition implements GPUTransition { device: Device; diff --git a/modules/core/src/lib/attribute/attribute-transition-utils.ts b/modules/core/src/transitions/gpu-transition-utils.ts similarity index 59% rename from modules/core/src/lib/attribute/attribute-transition-utils.ts rename to modules/core/src/transitions/gpu-transition-utils.ts index ebc7172fffe..da71e62497a 100644 --- a/modules/core/src/lib/attribute/attribute-transition-utils.ts +++ b/modules/core/src/transitions/gpu-transition-utils.ts @@ -1,68 +1,9 @@ -import type {Device} from '@luma.gl/core'; -import type {Buffer} from '@luma.gl/core'; -import {padArray} from '../../utils/array-utils'; -import {NumericArray, TypedArray} from '../../types/types'; -import Attribute from './attribute'; -import type {BufferAccessor} from './data-column'; -import {VertexFormat as LumaVertexFormat} from '@luma.gl/core'; - -export interface TransitionSettings { - type: string; - /** Callback to get the value that the entering vertices are transitioning from. */ - enter?: (toValue: NumericArray, chunk?: NumericArray) => NumericArray; - /** Callback when the transition is started */ - onStart?: () => void; - /** Callback when the transition is done */ - onEnd?: () => void; - /** Callback when the transition is interrupted */ - onInterrupt?: () => void; -} - -export type InterpolationTransitionSettings = TransitionSettings & { - type?: 'interpolation'; - /** Duration of the transition animation, in milliseconds */ - duration: number; - /** Easing function that maps a value from [0, 1] to [0, 1], see [http://easings.net/](http://easings.net/) */ - easing?: (t: number) => number; -}; - -export type SpringTransitionSettings = TransitionSettings & { - type: 'spring'; - /** "Tension" factor for the spring */ - stiffness: number; - /** "Friction" factor that counteracts the spring's acceleration */ - damping: number; -}; - -const DEFAULT_TRANSITION_SETTINGS = { - interpolation: { - duration: 0, - easing: t => t - }, - spring: { - stiffness: 0.05, - damping: 0.5 - } -}; - -export function normalizeTransitionSettings( - userSettings: number | InterpolationTransitionSettings | SpringTransitionSettings, - layerSettings?: boolean | Partial -): TransitionSettings | null { - if (!userSettings) { - return null; - } - if (Number.isFinite(userSettings)) { - userSettings = {type: 'interpolation', duration: userSettings as number}; - } - const type = (userSettings as TransitionSettings).type || 'interpolation'; - return { - ...DEFAULT_TRANSITION_SETTINGS[type], - ...(layerSettings as TransitionSettings), - ...(userSettings as TransitionSettings), - type - }; -} +import type {Device, Buffer, VertexFormat} from '@luma.gl/core'; +import {padArray} from '../utils/array-utils'; +import {NumericArray, TypedArray, TypedArrayConstructor} from '../types/types'; +import Attribute from '../lib/attribute/attribute'; +import type {BufferAccessor} from '../lib/attribute/data-column'; +import {GL} from '@luma.gl/constants'; // NOTE: NOT COPYING OVER OFFSET OR STRIDE HERE BECAUSE: // (1) WE DON'T SUPPORT INTERLEAVED BUFFERS FOR TRANSITIONS @@ -109,7 +50,7 @@ export function getAttributeTypeFromSize(size: number): string { } /** Returns the {@link VertexFormat} for the given number of float32 components. */ -export function getFloat32VertexFormat(size: 1 | 2 | 3 | 4): LumaVertexFormat { +export function getFloat32VertexFormat(size: number): VertexFormat { switch (size) { case 1: return 'float32'; @@ -131,7 +72,17 @@ export function cycleBuffers(buffers: Buffer[]): void { export function getAttributeBufferLength(attribute: Attribute, numInstances: number): number { const {doublePrecision, settings, value, size} = attribute; const multiplier = doublePrecision && value instanceof Float64Array ? 2 : 1; - return (settings.noAlloc ? (value as NumericArray).length : numInstances * size) * multiplier; + let maxVertexOffset = 0; + const {shaderAttributes} = attribute.settings; + if (shaderAttributes) { + for (const shaderAttribute of Object.values(shaderAttributes)) { + maxVertexOffset = Math.max(maxVertexOffset, shaderAttribute.vertexOffset ?? 0); + } + } + return ( + (settings.noAlloc ? (value as NumericArray).length : (numInstances + maxVertexOffset) * size) * + multiplier + ); } // This helper is used when transitioning attributes from a set of values in one buffer layout @@ -146,16 +97,16 @@ export function getAttributeBufferLength(attribute: Attribute, numInstances: num // was insufficient. Callers are responsible for disposing of the original buffer if needed. export function padBuffer({ buffer, - numInstances, attribute, fromLength, + toLength, fromStartIndices, getData = x => x }: { buffer: Buffer; - numInstances: number; attribute: Attribute; fromLength: number; + toLength: number; fromStartIndices?: NumericArray | null; getData?: (toValue: NumericArray, chunk?: NumericArray) => NumericArray; }): Buffer { @@ -165,9 +116,12 @@ export function padBuffer({ attribute.doublePrecision && attribute.value instanceof Float64Array ? 2 : 1; const size = attribute.size * precisionMultiplier; const byteOffset = attribute.byteOffset; + const targetByteOffset = + attribute.settings.bytesPerElement < 4 + ? (byteOffset / attribute.settings.bytesPerElement) * 4 + : byteOffset; const toStartIndices = attribute.startIndices; const hasStartIndices = fromStartIndices && toStartIndices; - const toLength = getAttributeBufferLength(attribute, numInstances); const isConstant = attribute.isConstant; // check if buffer needs to be padded @@ -175,21 +129,30 @@ export function padBuffer({ return buffer; } + const ArrayType = + attribute.value instanceof Float64Array + ? Float32Array + : ((attribute.value as TypedArray).constructor as TypedArrayConstructor); const toData = isConstant ? (attribute.value as TypedArray) : // TODO(v9.1): Avoid non-portable synchronous reads. - toFloat32Array(attribute.getBuffer()!.readSyncWebGL()); + new ArrayType( + attribute + .getBuffer()! + .readSyncWebGL(byteOffset, toLength * ArrayType.BYTES_PER_ELEMENT).buffer + ); if (attribute.settings.normalized && !isConstant) { const getter = getData; getData = (value, chunk) => attribute.normalizeConstant(getter(value, chunk)); } const getMissingData = isConstant - ? (i, chunk) => getData(toData, chunk) - : (i, chunk) => getData(toData.subarray(i + byteOffset, i + byteOffset + size), chunk); + ? (i: number, chunk: NumericArray) => getData(toData, chunk) + : (i: number, chunk: NumericArray) => + getData(toData.subarray(i + byteOffset, i + byteOffset + size), chunk); // TODO(v9.1): Avoid non-portable synchronous reads. - const source = toFloat32Array(buffer.readSyncWebGL()); + const source = new Float32Array(buffer.readSyncWebGL(0, fromLength * 4).buffer); const target = new Float32Array(toLength); padArray({ source, @@ -200,17 +163,12 @@ export function padBuffer({ getData: getMissingData }); - if (buffer.byteLength < target.byteLength + byteOffset) { - buffer = buffer.device.createBuffer({byteLength: target.byteLength + byteOffset}); + if (buffer.byteLength < target.byteLength + targetByteOffset) { + buffer = buffer.device.createBuffer({ + byteLength: target.byteLength + targetByteOffset, + usage: GL.DYNAMIC_COPY + }); } - buffer.write(target, byteOffset); + buffer.write(target, targetByteOffset); return buffer; } - -function toFloat32Array(bytes: Uint8Array): Float32Array { - return new Float32Array( - bytes.buffer, - bytes.byteOffset, - bytes.byteLength / Float32Array.BYTES_PER_ELEMENT - ); -} diff --git a/modules/core/src/transitions/gpu-transition.ts b/modules/core/src/transitions/gpu-transition.ts index 450daf3efea..b21266ff2e8 100644 --- a/modules/core/src/transitions/gpu-transition.ts +++ b/modules/core/src/transitions/gpu-transition.ts @@ -1,7 +1,7 @@ import type Attribute from '../lib/attribute/attribute'; -import type {TransitionSettings} from '../lib/attribute/attribute-transition-utils'; +import type {TransitionSettings} from './transition-settings'; -export default interface GPUTransition { +export interface GPUTransition { get type(): string; get inProgress(): boolean; get attributeInTransition(): Attribute; diff --git a/modules/core/src/transitions/transition-settings.ts b/modules/core/src/transitions/transition-settings.ts new file mode 100644 index 00000000000..abec1538308 --- /dev/null +++ b/modules/core/src/transitions/transition-settings.ts @@ -0,0 +1,59 @@ +import {NumericArray} from '../types/types'; + +export interface TransitionSettings { + type: string; + /** Callback to get the value that the entering vertices are transitioning from. */ + enter?: (toValue: NumericArray, chunk?: NumericArray) => NumericArray; + /** Callback when the transition is started */ + onStart?: () => void; + /** Callback when the transition is done */ + onEnd?: () => void; + /** Callback when the transition is interrupted */ + onInterrupt?: () => void; +} + +export type InterpolationTransitionSettings = TransitionSettings & { + type?: 'interpolation'; + /** Duration of the transition animation, in milliseconds */ + duration: number; + /** Easing function that maps a value from [0, 1] to [0, 1], see [http://easings.net/](http://easings.net/) */ + easing?: (t: number) => number; +}; + +export type SpringTransitionSettings = TransitionSettings & { + type: 'spring'; + /** "Tension" factor for the spring */ + stiffness: number; + /** "Friction" factor that counteracts the spring's acceleration */ + damping: number; +}; + +const DEFAULT_TRANSITION_SETTINGS = { + interpolation: { + duration: 0, + easing: t => t + }, + spring: { + stiffness: 0.05, + damping: 0.5 + } +}; + +export function normalizeTransitionSettings( + userSettings: number | InterpolationTransitionSettings | SpringTransitionSettings, + layerSettings?: boolean | Partial +): TransitionSettings | null { + if (!userSettings) { + return null; + } + if (Number.isFinite(userSettings)) { + userSettings = {type: 'interpolation', duration: userSettings as number}; + } + const type = (userSettings as TransitionSettings).type || 'interpolation'; + return { + ...DEFAULT_TRANSITION_SETTINGS[type], + ...(layerSettings as TransitionSettings), + ...(userSettings as TransitionSettings), + type + }; +} diff --git a/modules/core/src/utils/array-utils.ts b/modules/core/src/utils/array-utils.ts index 5bf6305a250..30ffb78c752 100644 --- a/modules/core/src/utils/array-utils.ts +++ b/modules/core/src/utils/array-utils.ts @@ -17,17 +17,24 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import type {NumericArray, TypedArray} from '../types/types'; /* * Helper function for padArray */ function padArrayChunk(options: { - source; - target; + /** original data */ + source: TypedArray; + /** output data */ + target: TypedArray; + /** length per datum */ + size: number; + /** callback to get new data when source is short */ + getData: (index: number, context: NumericArray) => NumericArray; + /** start index */ start?: number; + /** end index */ end?: number; - size: number; - getData; }): void { const {source, target, start = 0, size, getData} = options; const end = options.end || target.length; @@ -62,15 +69,29 @@ function padArrayChunk(options: { The arrays can have internal structures (like the attributes of PathLayer and SolidPolygonLayer), defined by the optional sourceStartIndices and targetStartIndices parameters. If the target array is larger, the getData callback is used to fill in the blanks. - * @params {TypedArray} source - original data - * @params {TypedArray} target - output data - * @params {Number} size - length per datum - * @params {Function} getData - callback to get new data when source is short - * @params {Array} [sourceStartIndices] - subdivision of the original data in [object0StartIndex, object1StartIndex, ...] - * @params {Array} [targetStartIndices] - subdivision of the output data in [object0StartIndex, object1StartIndex, ...] */ -export function padArray({source, target, size, getData, sourceStartIndices, targetStartIndices}) { - if (!Array.isArray(targetStartIndices)) { +export function padArray({ + source, + target, + size, + getData, + sourceStartIndices, + targetStartIndices +}: { + /** original data */ + source: TypedArray; + /** output data */ + target: TypedArray; + /** length per datum */ + size: number; + /** callback to get new data when source is short */ + getData: (index: number, context: NumericArray) => NumericArray; + /** subdivision of the original data in [object0StartIndex, object1StartIndex, ...] */ + sourceStartIndices?: NumericArray | null; + /** subdivision of the output data in [object0StartIndex, object1StartIndex, ...] */ + targetStartIndices?: NumericArray | null; +}): TypedArray { + if (!sourceStartIndices || !targetStartIndices) { // Flat arrays padArrayChunk({ source, @@ -107,6 +128,7 @@ export function padArray({source, target, size, getData, sourceStartIndices, tar if (targetIndex < target.length) { padArrayChunk({ + // @ts-ignore source: [], target, start: targetIndex, From 252b7cbc344547b72810d5bc6b5680d6e1c03d30 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 12:46:07 -0700 Subject: [PATCH 03/12] wip --- .../gpu-interpolation-transition.ts | 15 +- .../src/transitions/gpu-spring-transition.ts | 134 +++++++++--------- test/apps/attribute-transition/app.jsx | 2 +- 3 files changed, 71 insertions(+), 80 deletions(-) diff --git a/modules/core/src/transitions/gpu-interpolation-transition.ts b/modules/core/src/transitions/gpu-interpolation-transition.ts index d6a18c0eba1..1e98a40e8fa 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -1,7 +1,5 @@ -import type {Device, VertexFormat} from '@luma.gl/core'; +import type {Device, Buffer} from '@luma.gl/core'; import {Timeline, BufferTransform} from '@luma.gl/engine'; -import {Buffer} from '@luma.gl/core'; -import {GL} from '@luma.gl/constants'; import Attribute from '../lib/attribute/attribute'; import { getAttributeTypeFromSize, @@ -82,16 +80,15 @@ export default class GPUInterpolationTransition implements GPUTransition { cycleBuffers(buffers); const toLength = getAttributeBufferLength(attribute, numInstances); - const padBufferOpts = { - numInstances, + + const fromBuffer = padBuffer({ + buffer: buffers[0], attribute, fromLength: this.currentLength, toLength, fromStartIndices: this.currentStartIndices, getData: transitionSettings.enter - }; - - const fromBuffer = padBuffer({buffer: buffers[0], ...padBufferOpts}); + }); if (fromBuffer !== buffers[0]) { buffers[0].destroy(); buffers[0] = fromBuffer; @@ -115,7 +112,7 @@ export default class GPUInterpolationTransition implements GPUTransition { this.transition.start(transitionSettings); const {model} = this.transform; - model.setVertexCount(Math.floor(this.currentLength / attribute.size)); + model.setVertexCount(Math.floor(toLength / attribute.size)); if (attribute.isConstant) { model.setAttributes({aFrom: buffers[0]}); model.setConstantAttributes({aTo: attribute.value as TypedArray}); diff --git a/modules/core/src/transitions/gpu-spring-transition.ts b/modules/core/src/transitions/gpu-spring-transition.ts index 27ed202fe4f..0cdf2e5970c 100644 --- a/modules/core/src/transitions/gpu-spring-transition.ts +++ b/modules/core/src/transitions/gpu-spring-transition.ts @@ -1,7 +1,9 @@ /* eslint-disable complexity, max-statements, max-params */ -import type {Device} from '@luma.gl/core'; -import {BufferTransform} from '@luma.gl/engine'; -import {GL} from '@luma.gl/constants'; +import type {Device, + Buffer, + Framebuffer, + Texture} from '@luma.gl/core'; +import {Timeline, BufferTransform} from '@luma.gl/engine'; import { padBuffer, getAttributeTypeFromSize, @@ -13,14 +15,7 @@ import Attribute from '../lib/attribute/attribute'; import Transition from './transition'; import type {SpringTransitionSettings} from './transition-settings'; -import type {Timeline} from '@luma.gl/engine'; -import type {BufferTransform as LumaTransform} from '@luma.gl/engine'; -import type { - Buffer as LumaBuffer, - Framebuffer as LumaFramebuffer, - Texture as LumaTexture2D -} from '@luma.gl/core'; -import type {NumericArray} from '../types/types'; +import type {NumericArray, TypedArray} from '../types/types'; import type {GPUTransition} from './gpu-transition'; export default class GPUSpringTransition implements GPUTransition { @@ -33,10 +28,10 @@ export default class GPUSpringTransition implements GPUTransition { private transition: Transition; private currentStartIndices: NumericArray | null; private currentLength: number; - private texture: LumaTexture2D; - private framebuffer: LumaFramebuffer; - private transform: LumaTransform; - private buffers: [LumaBuffer, LumaBuffer, LumaBuffer]; + private texture: Texture; + private framebuffer: Framebuffer; + private transform: BufferTransform; + private buffers: Buffer[]; constructor({ device, @@ -48,14 +43,17 @@ export default class GPUSpringTransition implements GPUTransition { timeline: Timeline; }) { this.device = device; - this.type = 'spring'; this.transition = new Transition(timeline); this.attribute = attribute; // this is the attribute we return during the transition - note: if it is a constant // attribute, it will be converted and returned as a regular attribute - // `attribute.userData` is the original options passed when constructing the attribute. + // `attribute.settings` is the original options passed when constructing the attribute. // This ensures that we set the proper `doublePrecision` flag and shader attributes. this.attributeInTransition = new Attribute(device, attribute.settings); + // Placeholder value - necessary for generating the correct buffer layout + this.attributeInTransition.setData( + attribute.value instanceof Float64Array ? new Float64Array(0) : new Float32Array(0) + ); this.currentStartIndices = attribute.startIndices; // storing currentLength because this.buffer may be larger than the actual length we want to use // this is because we only reallocate buffers when they grow, not when they shrink, @@ -63,16 +61,11 @@ export default class GPUSpringTransition implements GPUTransition { this.currentLength = 0; this.texture = getTexture(device); this.framebuffer = getFramebuffer(device, this.texture); - const bufferOpts = { - byteLength: 0, - usage: GL.DYNAMIC_COPY - }; this.buffers = [ - device.createBuffer(bufferOpts), // previous - device.createBuffer(bufferOpts), // current - device.createBuffer(bufferOpts) // next + device.createBuffer({byteLength: 0}), + device.createBuffer({byteLength: 0}) ]; - this.transform = getTransform(device, attribute, this.buffers); + this.transform = getTransform(device, attribute); } get inProgress(): boolean { @@ -85,39 +78,38 @@ export default class GPUSpringTransition implements GPUTransition { // in case the attribute's buffer has changed in length or in // startIndices start(transitionSettings: SpringTransitionSettings, numInstances: number): void { + this.settings = transitionSettings; const {buffers, attribute} = this; - const padBufferOpts = { - numInstances, - attribute, - fromLength: this.currentLength, - fromStartIndices: this.currentStartIndices, - getData: transitionSettings.enter - }; - - for (const [index, buffer] of buffers.entries()) { - const paddedBuffer = padBuffer({buffer, ...padBufferOpts}); - - if (buffer !== paddedBuffer) { - buffer.destroy(); - buffers[index] = paddedBuffer; - - // TODO(v9): While this probably isn't necessary as a user-facing warning, it is helpful - // for debugging buffer allocation during deck.gl v9 development. - console.warn( - `[GPUSpringTransition] Replaced buffer ${buffer.id} (${buffer.byteLength} bytes) → ` + - `${paddedBuffer.id} (${paddedBuffer.byteLength} bytes)` - ); + const toLength = getAttributeBufferLength(attribute, numInstances); + + for (let i = 0; i < 2; i++) { + const paddedBuffer = padBuffer({ + buffer: buffers[i], + attribute, + fromLength: this.currentLength, + toLength, + fromStartIndices: this.currentStartIndices, + getData: transitionSettings.enter + }); + if (paddedBuffer !== buffers[i]) { + buffers[i].destroy(); + buffers[i] = paddedBuffer; } } + if (!buffers[2] || buffers[2].byteLength < buffers[0].byteLength) { + buffers[2]?.destroy(); + buffers[2] = this.device.createBuffer({ + byteLength: buffers[0].byteLength, + usage: buffers[0].usage + }); + } - this.settings = transitionSettings; this.currentStartIndices = attribute.startIndices; - this.currentLength = getAttributeBufferLength(attribute, numInstances); + this.currentLength = toLength; this.attributeInTransition.setData({ buffer: buffers[1], - // Hack: Float64Array is required for double-precision attributes - // to generate correct shader attributes - value: attribute.value as NumericArray + // Retain placeholder value to generate correct shader layout + value: this.attributeInTransition.value as NumericArray }); // when an attribute changes values, a new transition is started. These @@ -126,8 +118,13 @@ export default class GPUSpringTransition implements GPUTransition { // this.transition.start() takes the latest settings and updates them. this.transition.start({...transitionSettings, duration: Infinity}); - this.transform.model.setVertexCount(Math.floor(this.currentLength / attribute.size)); - this.transform.model.setAttributes({aTo: attribute.buffer}); + const {model} = this.transform; + model.setVertexCount(Math.floor(toLength / attribute.size)); + if (attribute.isConstant) { + model.setConstantAttributes({aTo: attribute.value as TypedArray}); + } else { + model.setAttributes({aTo: attribute.getBuffer()!}); + } } update() { @@ -138,7 +135,10 @@ export default class GPUSpringTransition implements GPUTransition { } const settings = this.settings as SpringTransitionSettings; - this.transform.model.setAttributes({aPrev: buffers[0], aCur: buffers[1]}); + transform.model.setAttributes({ + aPrev: buffers[0], + aCur: buffers[1] + }); this.transform.transformFeedback.setBuffers({vNext: buffers[2]}); transform.model.setUniforms({ stiffness: settings.stiffness, @@ -170,13 +170,13 @@ export default class GPUSpringTransition implements GPUTransition { cancel() { this.transition.cancel(); - this.transform.delete(); + this.transform.destroy(); for (const buffer of this.buffers) { - buffer.delete(); + buffer.destroy(); } - (this.buffers as LumaBuffer[]).length = 0; - this.texture.delete(); - this.framebuffer.delete(); + this.buffers.length = 0; + this.texture.destroy(); + this.framebuffer.destroy(); } } @@ -227,23 +227,17 @@ void main(void) { fragColor = vec4(1.0); }`; -function getTransform( - device: Device, - attribute: Attribute, - buffers: [LumaBuffer, LumaBuffer, LumaBuffer] -): LumaTransform { +function getTransform(device: Device, attribute: Attribute): BufferTransform { const attributeType = getAttributeTypeFromSize(attribute.size); - const format = getFloat32VertexFormat(attribute.size as 1 | 2 | 3 | 4); + const format = getFloat32VertexFormat(attribute.size); return new BufferTransform(device, { vs, fs, - attributes: {aPrev: buffers[0], aCur: buffers[1]}, bufferLayout: [ {name: 'aPrev', format}, {name: 'aCur', format}, - {name: 'aTo', format} + {name: 'aTo', format: attribute.getBufferLayout().attributes![0].format} ], - feedbackBuffers: {vNext: buffers[2]}, varyings: ['vNext'], defines: {ATTRIBUTE_TYPE: attributeType}, parameters: { @@ -258,7 +252,7 @@ function getTransform( }); } -function getTexture(device: Device): LumaTexture2D { +function getTexture(device: Device): Texture { return device.createTexture({ data: new Uint8Array(4), format: 'rgba8unorm', @@ -269,7 +263,7 @@ function getTexture(device: Device): LumaTexture2D { }); } -function getFramebuffer(device: Device, texture: LumaTexture2D): LumaFramebuffer { +function getFramebuffer(device: Device, texture: Texture): Framebuffer { return device.createFramebuffer({ id: 'spring-transition-is-transitioning-framebuffer', width: 1, diff --git a/test/apps/attribute-transition/app.jsx b/test/apps/attribute-transition/app.jsx index 6d215ed952a..38b653e4c87 100644 --- a/test/apps/attribute-transition/app.jsx +++ b/test/apps/attribute-transition/app.jsx @@ -77,7 +77,7 @@ function Root() { getPosition: d => d.position, getFillColor: d => d.color, getRadius: d => d.radius, - transitions: scatterplotTransitionsByType[transitionType] + // transitions: scatterplotTransitionsByType[transitionType] }), new PolygonLayer({ data: polygons, From c2cb953d33cc1049ba8a69baaaa536876e2be327 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 15:12:47 -0700 Subject: [PATCH 04/12] Fix spring transition buffer --- modules/core/src/transitions/gpu-spring-transition.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/core/src/transitions/gpu-spring-transition.ts b/modules/core/src/transitions/gpu-spring-transition.ts index 0cdf2e5970c..a8cbd2572d9 100644 --- a/modules/core/src/transitions/gpu-spring-transition.ts +++ b/modules/core/src/transitions/gpu-spring-transition.ts @@ -154,9 +154,8 @@ export default class GPUSpringTransition implements GPUTransition { cycleBuffers(buffers); this.attributeInTransition.setData({ buffer: buffers[1], - // Hack: Float64Array is required for double-precision attributes - // to generate correct shader attributes - value: this.attribute.value as NumericArray + // Retain placeholder value to generate correct shader layout + value: this.attributeInTransition.value as NumericArray }); const isTransitioning = this.device.readPixelsToArrayWebGL(framebuffer)[0] > 0; From 36304c0df367242f5ef77c53a57cedd26c0423be Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 16:33:55 -0700 Subject: [PATCH 05/12] narrow down on tf error --- .../core/src/transitions/gpu-interpolation-transition.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/core/src/transitions/gpu-interpolation-transition.ts b/modules/core/src/transitions/gpu-interpolation-transition.ts index 1e98a40e8fa..d41011c3d2a 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -136,8 +136,12 @@ export default class GPUInterpolationTransition implements GPUTransition { } const {model} = this.transform; model.setUniforms({time: t}); - // TODO - why is this needed? - model.setAttributes({aFrom: this.buffers[0]}); + // @ts-ignore + const gl = model.device.gl as WebGL2RenderingContext + // Work around for [.WebGL-0x12804417100] + // GL_INVALID_OPERATION: A transform feedback buffer that would be written to is also bound to a non-transform-feedback target + // TODO - luma should clean up after Model.setAttributes? + gl.bindBuffer(gl.ARRAY_BUFFER, null); this.transform.run(); } From 35de7fd9584211ed60e93643bbdca2ccfe6714f5 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 18:29:23 -0700 Subject: [PATCH 06/12] unblock model caching --- modules/core/src/transitions/gpu-interpolation-transition.ts | 5 +---- test/apps/attribute-transition/app.jsx | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/modules/core/src/transitions/gpu-interpolation-transition.ts b/modules/core/src/transitions/gpu-interpolation-transition.ts index d41011c3d2a..0bcc082c7af 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -138,9 +138,7 @@ export default class GPUInterpolationTransition implements GPUTransition { model.setUniforms({time: t}); // @ts-ignore const gl = model.device.gl as WebGL2RenderingContext - // Work around for [.WebGL-0x12804417100] - // GL_INVALID_OPERATION: A transform feedback buffer that would be written to is also bound to a non-transform-feedback target - // TODO - luma should clean up after Model.setAttributes? + // TODO - remove after https://github.com/visgl/luma.gl/pull/2023 gl.bindBuffer(gl.ARRAY_BUFFER, null); this.transform.run(); @@ -176,7 +174,6 @@ void main(void) { function getTransform(device: Device, attribute: Attribute): BufferTransform { const attributeType = getAttributeTypeFromSize(attribute.size); return new BufferTransform(device, { - id: `${attribute.id}-transition`, vs, bufferLayout: [ {name: 'aFrom', format: getFloat32VertexFormat(attribute.size)}, diff --git a/test/apps/attribute-transition/app.jsx b/test/apps/attribute-transition/app.jsx index 38b653e4c87..6d215ed952a 100644 --- a/test/apps/attribute-transition/app.jsx +++ b/test/apps/attribute-transition/app.jsx @@ -77,7 +77,7 @@ function Root() { getPosition: d => d.position, getFillColor: d => d.color, getRadius: d => d.radius, - // transitions: scatterplotTransitionsByType[transitionType] + transitions: scatterplotTransitionsByType[transitionType] }), new PolygonLayer({ data: polygons, From 72dbac8aac0a950f29a35ff526f9f38888dd5d82 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 18:48:49 -0700 Subject: [PATCH 07/12] lint --- .../src/transitions/gpu-interpolation-transition.ts | 2 +- modules/core/src/transitions/gpu-spring-transition.ts | 10 ++-------- modules/core/src/transitions/gpu-transition-utils.ts | 1 + 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/core/src/transitions/gpu-interpolation-transition.ts b/modules/core/src/transitions/gpu-interpolation-transition.ts index 0bcc082c7af..1dd65e367b6 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -137,7 +137,7 @@ export default class GPUInterpolationTransition implements GPUTransition { const {model} = this.transform; model.setUniforms({time: t}); // @ts-ignore - const gl = model.device.gl as WebGL2RenderingContext + const gl = model.device.gl as WebGL2RenderingContext; // TODO - remove after https://github.com/visgl/luma.gl/pull/2023 gl.bindBuffer(gl.ARRAY_BUFFER, null); diff --git a/modules/core/src/transitions/gpu-spring-transition.ts b/modules/core/src/transitions/gpu-spring-transition.ts index a8cbd2572d9..a1c3b128c11 100644 --- a/modules/core/src/transitions/gpu-spring-transition.ts +++ b/modules/core/src/transitions/gpu-spring-transition.ts @@ -1,8 +1,5 @@ /* eslint-disable complexity, max-statements, max-params */ -import type {Device, - Buffer, - Framebuffer, - Texture} from '@luma.gl/core'; +import type {Device, Buffer, Framebuffer, Texture} from '@luma.gl/core'; import {Timeline, BufferTransform} from '@luma.gl/engine'; import { padBuffer, @@ -61,10 +58,7 @@ export default class GPUSpringTransition implements GPUTransition { this.currentLength = 0; this.texture = getTexture(device); this.framebuffer = getFramebuffer(device, this.texture); - this.buffers = [ - device.createBuffer({byteLength: 0}), - device.createBuffer({byteLength: 0}) - ]; + this.buffers = [device.createBuffer({byteLength: 0}), device.createBuffer({byteLength: 0})]; this.transform = getTransform(device, attribute); } diff --git a/modules/core/src/transitions/gpu-transition-utils.ts b/modules/core/src/transitions/gpu-transition-utils.ts index da71e62497a..76704d5f68d 100644 --- a/modules/core/src/transitions/gpu-transition-utils.ts +++ b/modules/core/src/transitions/gpu-transition-utils.ts @@ -85,6 +85,7 @@ export function getAttributeBufferLength(attribute: Attribute, numInstances: num ); } +/* eslint-disable complexity */ // This helper is used when transitioning attributes from a set of values in one buffer layout // to a set of values in a different buffer layout. (Buffer layouts are used when attribute values // within a buffer should be grouped for drawElements, like the Polygon layer.) For example, a From 3b6265deef539ac2a2de6ebb284866a3ae0e3c6f Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 22:04:25 -0700 Subject: [PATCH 08/12] refactor and code reuse --- .../attribute/attribute-transition-manager.ts | 4 +- modules/core/src/lib/attribute/attribute.ts | 5 +- .../attribute}/transition-settings.ts | 2 +- .../src/lib/uniform-transition-manager.ts | 2 +- .../gpu-interpolation-transition.ts | 140 ++++++------------ .../src/transitions/gpu-spring-transition.ts | 120 ++++----------- .../src/transitions/gpu-transition-utils.ts | 71 +++++---- .../core/src/transitions/gpu-transition.ts | 91 +++++++++++- 8 files changed, 207 insertions(+), 228 deletions(-) rename modules/core/src/{transitions => lib/attribute}/transition-settings.ts (97%) diff --git a/modules/core/src/lib/attribute/attribute-transition-manager.ts b/modules/core/src/lib/attribute/attribute-transition-manager.ts index 1a1cd2cb106..3e32b0817ae 100644 --- a/modules/core/src/lib/attribute/attribute-transition-manager.ts +++ b/modules/core/src/lib/attribute/attribute-transition-manager.ts @@ -9,7 +9,7 @@ import type {Timeline} from '@luma.gl/engine'; import type {GPUTransition} from '../../transitions/gpu-transition'; import type {ConstructorOf} from '../../types/types'; import type Attribute from './attribute'; -import type {TransitionSettings} from '../../transitions/transition-settings'; +import type {TransitionSettings} from './transition-settings'; const TRANSITION_TYPES: Record> = { interpolation: GPUInterpolationTransition, @@ -129,7 +129,7 @@ export default class AttributeTransitionManager { /* Private methods */ private _removeTransition(attributeName: string): void { - this.transitions[attributeName].cancel(); + this.transitions[attributeName].delete(); delete this.transitions[attributeName]; } diff --git a/modules/core/src/lib/attribute/attribute.ts b/modules/core/src/lib/attribute/attribute.ts index fbf21d2d879..9199cd42a5e 100644 --- a/modules/core/src/lib/attribute/attribute.ts +++ b/modules/core/src/lib/attribute/attribute.ts @@ -10,10 +10,7 @@ import {createIterable, getAccessorFromBuffer} from '../../utils/iterable-utils' import {fillArray} from '../../utils/flatten'; import * as range from '../../utils/range'; import {bufferLayoutEqual} from './gl-utils'; -import { - normalizeTransitionSettings, - TransitionSettings -} from '../../transitions/transition-settings'; +import {normalizeTransitionSettings, TransitionSettings} from './transition-settings'; import type {Device, Buffer, BufferLayout} from '@luma.gl/core'; import type {NumericArray, TypedArray} from '../../types/types'; diff --git a/modules/core/src/transitions/transition-settings.ts b/modules/core/src/lib/attribute/transition-settings.ts similarity index 97% rename from modules/core/src/transitions/transition-settings.ts rename to modules/core/src/lib/attribute/transition-settings.ts index abec1538308..51daa25c02f 100644 --- a/modules/core/src/transitions/transition-settings.ts +++ b/modules/core/src/lib/attribute/transition-settings.ts @@ -1,4 +1,4 @@ -import {NumericArray} from '../types/types'; +import {NumericArray} from '../../types/types'; export interface TransitionSettings { type: string; diff --git a/modules/core/src/lib/uniform-transition-manager.ts b/modules/core/src/lib/uniform-transition-manager.ts index 17e062668f0..cfa28293e46 100644 --- a/modules/core/src/lib/uniform-transition-manager.ts +++ b/modules/core/src/lib/uniform-transition-manager.ts @@ -1,4 +1,4 @@ -import {normalizeTransitionSettings} from '../transitions/transition-settings'; +import {normalizeTransitionSettings} from './attribute/transition-settings'; import CPUInterpolationTransition from '../transitions/cpu-interpolation-transition'; import CPUSpringTransition from '../transitions/cpu-spring-transition'; import log from '../utils/log'; diff --git a/modules/core/src/transitions/gpu-interpolation-transition.ts b/modules/core/src/transitions/gpu-interpolation-transition.ts index 1dd65e367b6..cc3482d09ae 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -1,31 +1,22 @@ -import type {Device, Buffer} from '@luma.gl/core'; +import type {Device} from '@luma.gl/core'; import {Timeline, BufferTransform} from '@luma.gl/engine'; import Attribute from '../lib/attribute/attribute'; import { getAttributeTypeFromSize, - getAttributeBufferLength, cycleBuffers, padBuffer, + matchBuffer, getFloat32VertexFormat } from './gpu-transition-utils'; -import Transition from './transition'; +import {GPUTransitionBase} from './gpu-transition'; -import type {InterpolationTransitionSettings} from './transition-settings'; -import type {NumericArray, TypedArray} from '../types/types'; -import type {GPUTransition} from './gpu-transition'; +import type {InterpolationTransitionSettings} from '../lib/attribute/transition-settings'; +import type {TypedArray} from '../types/types'; -export default class GPUInterpolationTransition implements GPUTransition { - device: Device; +export default class GPUInterpolationTransition extends GPUTransitionBase { type = 'interpolation'; - attributeInTransition: Attribute; - private settings?: InterpolationTransitionSettings; - private attribute: Attribute; - private transition: Transition; - private currentStartIndices: NumericArray | null; - private currentLength: number; private transform: BufferTransform; - private buffers: Buffer[]; constructor({ device, @@ -36,42 +27,20 @@ export default class GPUInterpolationTransition implements GPUTransition { attribute: Attribute; timeline: Timeline; }) { - this.device = device; - this.transition = new Transition(timeline); - this.attribute = attribute; - // this is the attribute we return during the transition - note: if it is a constant - // attribute, it will be converted and returned as a regular attribute - // `attribute.settings` is the original options passed when constructing the attribute. - // This ensures that we set the proper `doublePrecision` flag and shader attributes. - this.attributeInTransition = new Attribute(device, attribute.settings); - // Placeholder value - necessary for generating the correct buffer layout - this.attributeInTransition.setData( - attribute.value instanceof Float64Array ? new Float64Array(0) : new Float32Array(0) - ); - this.currentStartIndices = attribute.startIndices; - // storing currentLength because this.buffer may be larger than the actual length we want to use - // this is because we only reallocate buffers when they grow, not when they shrink, - // due to performance costs - this.currentLength = 0; + super({device, attribute, timeline}); this.transform = getTransform(device, attribute); - this.buffers = [device.createBuffer({byteLength: 0})]; } - get inProgress(): boolean { - return this.transition.inProgress; - } + override start(transitionSettings: InterpolationTransitionSettings, numInstances: number): void { + const prevLength = this.currentLength; + const prevStartIndices = this.currentStartIndices; + + super.start(transitionSettings, numInstances, transitionSettings.duration); - // this is called when an attribute's values have changed and - // we need to start animating towards the new values - // this also correctly resizes / pads the transform's buffers - // in case the attribute's buffer has changed in length or in - // startIndices - start(transitionSettings: InterpolationTransitionSettings, numInstances: number): void { if (transitionSettings.duration <= 0) { this.transition.cancel(); return; } - this.settings = transitionSettings; const {buffers, attribute} = this; // Alternate between two buffers when new transitions start. @@ -79,40 +48,26 @@ export default class GPUInterpolationTransition implements GPUTransition { // And the other buffer is now the current buffer. cycleBuffers(buffers); - const toLength = getAttributeBufferLength(attribute, numInstances); - - const fromBuffer = padBuffer({ + buffers[0] = padBuffer({ + device: this.device, buffer: buffers[0], attribute, - fromLength: this.currentLength, - toLength, - fromStartIndices: this.currentStartIndices, + fromLength: prevLength, + toLength: this.currentLength, + fromStartIndices: prevStartIndices, getData: transitionSettings.enter }); - if (fromBuffer !== buffers[0]) { - buffers[0].destroy(); - buffers[0] = fromBuffer; - } - if (!buffers[1] || buffers[1].byteLength < fromBuffer.byteLength) { - buffers[1]?.destroy(); - buffers[1] = this.device.createBuffer({ - byteLength: fromBuffer.byteLength, - usage: fromBuffer.usage - }); - } - - this.currentStartIndices = attribute.startIndices; - this.currentLength = toLength; - this.attributeInTransition.setData({ - buffer: buffers[1], - // Retain placeholder value to generate correct shader layout - value: this.attributeInTransition.value as NumericArray + buffers[1] = matchBuffer({ + device: this.device, + source: buffers[0], + target: buffers[1] }); - this.transition.start(transitionSettings); + this.setBuffer(buffers[1]); - const {model} = this.transform; - model.setVertexCount(Math.floor(toLength / attribute.size)); + const {transform} = this; + const model = transform.model; + model.setVertexCount(Math.floor(this.currentLength / attribute.size)); if (attribute.isConstant) { model.setAttributes({aFrom: buffers[0]}); model.setConstantAttributes({aTo: attribute.value as TypedArray}); @@ -122,37 +77,32 @@ export default class GPUInterpolationTransition implements GPUTransition { aTo: attribute.getBuffer()! }); } - this.transform.transformFeedback.setBuffers({vCurrent: buffers[1]}); + transform.transformFeedback.setBuffers({vCurrent: buffers[1]}); } - update(): boolean { - const updated = this.transition.update(); - if (updated) { - const {duration, easing} = this.settings!; - const {time} = this.transition; - let t = time / duration; - if (easing) { - t = easing(t); - } - const {model} = this.transform; - model.setUniforms({time: t}); - // @ts-ignore - const gl = model.device.gl as WebGL2RenderingContext; - // TODO - remove after https://github.com/visgl/luma.gl/pull/2023 - gl.bindBuffer(gl.ARRAY_BUFFER, null); - - this.transform.run(); + onUpdate() { + const {duration, easing} = this.settings!; + const {time} = this.transition; + let t = time / duration; + if (easing) { + t = easing(t); } - return updated; + const {model} = this.transform; + model.setUniforms({time: t}); + // @ts-ignore + const gl = model.device.gl as WebGL2RenderingContext; + // TODO - remove after https://github.com/visgl/luma.gl/pull/2023 + gl.bindBuffer(gl.ARRAY_BUFFER, null); + + this.transform.run({ + clearColor: false, + clearDepth: false + }); } - cancel(): void { - this.transition.cancel(); + override delete() { + super.delete(); this.transform.destroy(); - for (const buffer of this.buffers) { - buffer.destroy(); - } - this.buffers.length = 0; } } diff --git a/modules/core/src/transitions/gpu-spring-transition.ts b/modules/core/src/transitions/gpu-spring-transition.ts index a1c3b128c11..825d48e89da 100644 --- a/modules/core/src/transitions/gpu-spring-transition.ts +++ b/modules/core/src/transitions/gpu-spring-transition.ts @@ -1,34 +1,24 @@ -/* eslint-disable complexity, max-statements, max-params */ -import type {Device, Buffer, Framebuffer, Texture} from '@luma.gl/core'; +import type {Device, Framebuffer, Texture} from '@luma.gl/core'; import {Timeline, BufferTransform} from '@luma.gl/engine'; import { padBuffer, + matchBuffer, getAttributeTypeFromSize, - getAttributeBufferLength, getFloat32VertexFormat, cycleBuffers } from './gpu-transition-utils'; import Attribute from '../lib/attribute/attribute'; -import Transition from './transition'; +import {GPUTransitionBase} from './gpu-transition'; -import type {SpringTransitionSettings} from './transition-settings'; -import type {NumericArray, TypedArray} from '../types/types'; -import type {GPUTransition} from './gpu-transition'; +import type {SpringTransitionSettings} from '../lib/attribute/transition-settings'; +import type {TypedArray} from '../types/types'; -export default class GPUSpringTransition implements GPUTransition { - device: Device; +export default class GPUSpringTransition extends GPUTransitionBase { type = 'spring'; - attributeInTransition: Attribute; - private settings?: SpringTransitionSettings; - private attribute: Attribute; - private transition: Transition; - private currentStartIndices: NumericArray | null; - private currentLength: number; private texture: Texture; private framebuffer: Framebuffer; private transform: BufferTransform; - private buffers: Buffer[]; constructor({ device, @@ -39,81 +29,40 @@ export default class GPUSpringTransition implements GPUTransition { attribute: Attribute; timeline: Timeline; }) { - this.device = device; - this.transition = new Transition(timeline); - this.attribute = attribute; - // this is the attribute we return during the transition - note: if it is a constant - // attribute, it will be converted and returned as a regular attribute - // `attribute.settings` is the original options passed when constructing the attribute. - // This ensures that we set the proper `doublePrecision` flag and shader attributes. - this.attributeInTransition = new Attribute(device, attribute.settings); - // Placeholder value - necessary for generating the correct buffer layout - this.attributeInTransition.setData( - attribute.value instanceof Float64Array ? new Float64Array(0) : new Float32Array(0) - ); - this.currentStartIndices = attribute.startIndices; - // storing currentLength because this.buffer may be larger than the actual length we want to use - // this is because we only reallocate buffers when they grow, not when they shrink, - // due to performance costs - this.currentLength = 0; + super({device, attribute, timeline}); this.texture = getTexture(device); this.framebuffer = getFramebuffer(device, this.texture); - this.buffers = [device.createBuffer({byteLength: 0}), device.createBuffer({byteLength: 0})]; this.transform = getTransform(device, attribute); } - get inProgress(): boolean { - return this.transition.inProgress; - } + override start(transitionSettings: SpringTransitionSettings, numInstances: number): void { + const prevLength = this.currentLength; + const prevStartIndices = this.currentStartIndices; + super.start(transitionSettings, numInstances); - // this is called when an attribute's values have changed and - // we need to start animating towards the new values - // this also correctly resizes / pads the transform's buffers - // in case the attribute's buffer has changed in length or in - // startIndices - start(transitionSettings: SpringTransitionSettings, numInstances: number): void { - this.settings = transitionSettings; const {buffers, attribute} = this; - const toLength = getAttributeBufferLength(attribute, numInstances); for (let i = 0; i < 2; i++) { - const paddedBuffer = padBuffer({ + buffers[i] = padBuffer({ + device: this.device, buffer: buffers[i], attribute, - fromLength: this.currentLength, - toLength, - fromStartIndices: this.currentStartIndices, + fromLength: prevLength, + toLength: this.currentLength, + fromStartIndices: prevStartIndices, getData: transitionSettings.enter }); - if (paddedBuffer !== buffers[i]) { - buffers[i].destroy(); - buffers[i] = paddedBuffer; - } - } - if (!buffers[2] || buffers[2].byteLength < buffers[0].byteLength) { - buffers[2]?.destroy(); - buffers[2] = this.device.createBuffer({ - byteLength: buffers[0].byteLength, - usage: buffers[0].usage - }); } - - this.currentStartIndices = attribute.startIndices; - this.currentLength = toLength; - this.attributeInTransition.setData({ - buffer: buffers[1], - // Retain placeholder value to generate correct shader layout - value: this.attributeInTransition.value as NumericArray + buffers[2] = matchBuffer({ + device: this.device, + source: buffers[0], + target: buffers[2] }); - // when an attribute changes values, a new transition is started. These - // are properties that we have to store on this.transition but can change - // when new transitions are started, so we have to keep them up-to-date. - // this.transition.start() takes the latest settings and updates them. - this.transition.start({...transitionSettings, duration: Infinity}); + this.setBuffer(buffers[1]); const {model} = this.transform; - model.setVertexCount(Math.floor(toLength / attribute.size)); + model.setVertexCount(Math.floor(this.currentLength / attribute.size)); if (attribute.isConstant) { model.setConstantAttributes({aTo: attribute.value as TypedArray}); } else { @@ -121,19 +70,16 @@ export default class GPUSpringTransition implements GPUTransition { } } - update() { + onUpdate() { const {buffers, transform, framebuffer, transition} = this; - const updated = transition.update(); - if (!updated) { - return false; - } + const settings = this.settings as SpringTransitionSettings; transform.model.setAttributes({ aPrev: buffers[0], aCur: buffers[1] }); - this.transform.transformFeedback.setBuffers({vNext: buffers[2]}); + transform.transformFeedback.setBuffers({vNext: buffers[2]}); transform.model.setUniforms({ stiffness: settings.stiffness, damping: settings.damping @@ -146,28 +92,18 @@ export default class GPUSpringTransition implements GPUTransition { }); cycleBuffers(buffers); - this.attributeInTransition.setData({ - buffer: buffers[1], - // Retain placeholder value to generate correct shader layout - value: this.attributeInTransition.value as NumericArray - }); + this.setBuffer(buffers[1]); const isTransitioning = this.device.readPixelsToArrayWebGL(framebuffer)[0] > 0; if (!isTransitioning) { transition.end(); } - - return true; } - cancel() { - this.transition.cancel(); + override delete() { + super.delete(); this.transform.destroy(); - for (const buffer of this.buffers) { - buffer.destroy(); - } - this.buffers.length = 0; this.texture.destroy(); this.framebuffer.destroy(); } diff --git a/modules/core/src/transitions/gpu-transition-utils.ts b/modules/core/src/transitions/gpu-transition-utils.ts index 76704d5f68d..9bd810d7874 100644 --- a/modules/core/src/transitions/gpu-transition-utils.ts +++ b/modules/core/src/transitions/gpu-transition-utils.ts @@ -2,35 +2,18 @@ import type {Device, Buffer, VertexFormat} from '@luma.gl/core'; import {padArray} from '../utils/array-utils'; import {NumericArray, TypedArray, TypedArrayConstructor} from '../types/types'; import Attribute from '../lib/attribute/attribute'; -import type {BufferAccessor} from '../lib/attribute/data-column'; import {GL} from '@luma.gl/constants'; -// NOTE: NOT COPYING OVER OFFSET OR STRIDE HERE BECAUSE: -// (1) WE DON'T SUPPORT INTERLEAVED BUFFERS FOR TRANSITIONS -// (2) BUFFERS WITH OFFSETS ALWAYS CONTAIN VALUES OF THE SAME SIZE -// (3) THE OPERATIONS IN THE SHADER ARE PER-COMPONENT (addition and scaling) -export function getSourceBufferAttribute( - device: Device, - attribute: Attribute -): [Buffer, BufferAccessor] | NumericArray { - // The Attribute we pass to Transform as a sourceBuffer must have {divisor: 0} - // so we create a copy of the attribute (with divisor=0) to use when running - // transform feedback - const buffer = attribute.getBuffer(); - if (buffer) { - return [ - buffer, - { - divisor: 0, - size: attribute.size, - normalized: attribute.settings.normalized - } as BufferAccessor - ]; - } - // constant - // don't pass normalized here because the `value` from a normalized attribute is - // already normalized - return attribute.value as NumericArray; +/** Create a new empty attribute with the same settings: type, shader layout etc. */ +export function cloneAttribute(attribute: Attribute): Attribute { + // `attribute.settings` is the original options passed when constructing the attribute. + // This ensures that we set the proper `doublePrecision` flag and shader attributes. + const newAttribute = new Attribute(attribute.device, attribute.settings); + // Placeholder value - necessary for generating the correct buffer layout + newAttribute.setData( + attribute.value instanceof Float64Array ? new Float64Array(0) : new Float32Array(0) + ); + return newAttribute; } /** Returns the GLSL attribute type for the given number of float32 components. */ @@ -85,6 +68,25 @@ export function getAttributeBufferLength(attribute: Attribute, numInstances: num ); } +export function matchBuffer({ + device, + source, + target +}: { + device: Device; + source: Buffer; + target?: Buffer; +}): Buffer { + if (!target || target.byteLength < source.byteLength) { + target?.destroy(); + target = device.createBuffer({ + byteLength: source.byteLength, + usage: source.usage + }); + } + return target; +} + /* eslint-disable complexity */ // This helper is used when transitioning attributes from a set of values in one buffer layout // to a set of values in a different buffer layout. (Buffer layouts are used when attribute values @@ -97,6 +99,7 @@ export function getAttributeBufferLength(attribute: Attribute, numInstances: num // padBuffer may return either the original buffer, or a new buffer if the size of the original // was insufficient. Callers are responsible for disposing of the original buffer if needed. export function padBuffer({ + device, buffer, attribute, fromLength, @@ -104,7 +107,8 @@ export function padBuffer({ fromStartIndices, getData = x => x }: { - buffer: Buffer; + device: Device; + buffer?: Buffer; attribute: Attribute; fromLength: number; toLength: number; @@ -126,7 +130,7 @@ export function padBuffer({ const isConstant = attribute.isConstant; // check if buffer needs to be padded - if (!hasStartIndices && fromLength >= toLength) { + if (!hasStartIndices && buffer && fromLength >= toLength) { return buffer; } @@ -153,7 +157,9 @@ export function padBuffer({ getData(toData.subarray(i + byteOffset, i + byteOffset + size), chunk); // TODO(v9.1): Avoid non-portable synchronous reads. - const source = new Float32Array(buffer.readSyncWebGL(0, fromLength * 4).buffer); + const source = buffer + ? new Float32Array(buffer.readSyncWebGL(targetByteOffset, fromLength * 4).buffer) + : new Float32Array(0); const target = new Float32Array(toLength); padArray({ source, @@ -164,8 +170,9 @@ export function padBuffer({ getData: getMissingData }); - if (buffer.byteLength < target.byteLength + targetByteOffset) { - buffer = buffer.device.createBuffer({ + if (!buffer || buffer.byteLength < target.byteLength + targetByteOffset) { + buffer?.destroy(); + buffer = device.createBuffer({ byteLength: target.byteLength + targetByteOffset, usage: GL.DYNAMIC_COPY }); diff --git a/modules/core/src/transitions/gpu-transition.ts b/modules/core/src/transitions/gpu-transition.ts index b21266ff2e8..d6017c7d534 100644 --- a/modules/core/src/transitions/gpu-transition.ts +++ b/modules/core/src/transitions/gpu-transition.ts @@ -1,12 +1,101 @@ +import Transition from './transition'; +import {cloneAttribute, getAttributeBufferLength} from './gpu-transition-utils'; + +import type {Device, Buffer} from '@luma.gl/core'; +import type {Timeline} from '@luma.gl/engine'; import type Attribute from '../lib/attribute/attribute'; -import type {TransitionSettings} from './transition-settings'; +import type {TransitionSettings} from '../lib/attribute/transition-settings'; +import type {NumericArray} from '../types/types'; export interface GPUTransition { get type(): string; get inProgress(): boolean; get attributeInTransition(): Attribute; + /** Called when an attribute's values have changed and we need to start animating towards the new values */ start(transitionSettings: TransitionSettings, numInstances: number): void; + /** Called while transition is in progress */ update(): boolean; + /** Called when transition is interrupted */ cancel(): void; + /** Called when transition is disposed */ + delete(): void; +} + +export abstract class GPUTransitionBase + implements GPUTransition +{ + abstract get type(): string; + + device: Device; + attribute: Attribute; + transition: Transition; + settings?: SettingsT; + /** The attribute that holds the buffer in transition */ + attributeInTransition: Attribute; + protected buffers: Buffer[] = []; + /** The vertex count of the last buffer. + * Buffer may be larger than the actual length we want to use + * because we only reallocate buffers when they grow, not when they shrink, + * due to performance costs */ + protected currentLength: number = 0; + /** The start indices of the last buffer. */ + protected currentStartIndices: NumericArray | null; + + constructor({ + device, + attribute, + timeline + }: { + device: Device; + attribute: Attribute; + timeline: Timeline; + }) { + this.device = device; + this.transition = new Transition(timeline); + this.attribute = attribute; + this.attributeInTransition = cloneAttribute(attribute); + this.currentStartIndices = attribute.startIndices; + } + + get inProgress(): boolean { + return this.transition.inProgress; + } + + start(transitionSettings: SettingsT, numInstances: number, duration: number = Infinity) { + this.settings = transitionSettings; + this.currentStartIndices = this.attribute.startIndices; + this.currentLength = getAttributeBufferLength(this.attribute, numInstances); + this.transition.start({...transitionSettings, duration}); + } + + update(): boolean { + const updated = this.transition.update(); + if (updated) { + this.onUpdate(); + } + return updated; + } + + abstract onUpdate(): void; + + protected setBuffer(buffer: Buffer) { + this.attributeInTransition.setData({ + buffer, + // Retain placeholder value to generate correct shader layout + value: this.attributeInTransition.value as NumericArray + }); + } + + cancel(): void { + this.transition.cancel(); + } + + delete(): void { + this.cancel(); + for (const buffer of this.buffers) { + buffer.destroy(); + } + this.buffers.length = 0; + } } From 569c24ac46ec64669ef0459d12f47fc84ae1da29 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 22:36:12 -0700 Subject: [PATCH 09/12] render test --- test/render/test-cases/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/render/test-cases/index.js b/test/render/test-cases/index.js index 7a4417a9601..3846e1444e5 100644 --- a/test/render/test-cases/index.js +++ b/test/render/test-cases/index.js @@ -58,7 +58,7 @@ export default [].concat( viewsTests, // effectsTests, // TODO - Broken in headless mode with Chrome 113 - // transitionTests, + transitionTests, terrainLayerTests, // collisionFilterExtensionTests dataFilterExtensionTests From 7c5e15b3df79da261363cc093fb592e11dca19ce Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 12 Mar 2024 23:05:24 -0700 Subject: [PATCH 10/12] fix tests --- .../src/transitions/gpu-transition-utils.ts | 2 ++ .../attribute-transition-manager.spec.ts | 20 +++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/modules/core/src/transitions/gpu-transition-utils.ts b/modules/core/src/transitions/gpu-transition-utils.ts index 9bd810d7874..612fb09799d 100644 --- a/modules/core/src/transitions/gpu-transition-utils.ts +++ b/modules/core/src/transitions/gpu-transition-utils.ts @@ -121,6 +121,8 @@ export function padBuffer({ attribute.doublePrecision && attribute.value instanceof Float64Array ? 2 : 1; const size = attribute.size * precisionMultiplier; const byteOffset = attribute.byteOffset; + // Transform feedback can only write to float varyings + // Attributes of format unorm8/uint8 (1 byte per element) etc will be padded to float32 (4 bytes per element) const targetByteOffset = attribute.settings.bytesPerElement < 4 ? (byteOffset / attribute.settings.bytesPerElement) * 4 diff --git a/test/modules/core/lib/attribute/attribute-transition-manager.spec.ts b/test/modules/core/lib/attribute/attribute-transition-manager.spec.ts index 5ec1605f4e1..7e37c0c8c2f 100644 --- a/test/modules/core/lib/attribute/attribute-transition-manager.spec.ts +++ b/test/modules/core/lib/attribute/attribute-transition-manager.spec.ts @@ -70,11 +70,9 @@ test('AttributeTransitionManager#update', async t => { t.ok(manager.hasAttribute('instanceSizes'), 'added transition for instanceSizes'); t.ok(manager.hasAttribute('instancePositions'), 'added transition for instancePositions'); - // TEST_ATTRIBUTES initializes 'instanceSizes' (4x floats). DataColumn adds padding (stride x 2). - // byteLength = numInstances * 4 + 8. Later reallocation may skip the padding. - + // byteLength = max(numInstances, 1) * 4. Later reallocation may skip the padding. const sizeTransition = manager.transitions.instanceSizes; - t.is(sizeTransition.buffers[0].byteLength, 4 * 4 + 8, 'buffer has correct size'); + t.is(sizeTransition.buffers[0].byteLength, 4, 'buffer has correct size'); const positionTransform = manager.transitions.instancePositions.transform; t.ok(positionTransform, 'transform is constructed for instancePositions'); @@ -84,22 +82,14 @@ test('AttributeTransitionManager#update', async t => { t.ok(manager.hasAttribute('instanceSizes'), 'added transition for instanceSizes'); t.notOk(manager.hasAttribute('instancePositions'), 'removed transition for instancePositions'); t.notOk(positionTransform._handle, 'instancePositions transform is deleted'); - t.is(sizeTransition.buffers[0].byteLength, 4 * 4 + 8, 'buffer has correct size'); - - // TODO(v9): Previous 'expected' values for these tests indicated that padding should be - // overwritten with new values. Padding is _not_ overwritten as of visgl/deck.gl#8425, but the - // PR strictly improves `test/apps/attribute-transition`. Test cases below merit a closer look, - // when resolving remaining bugs in attribute transitions for deck.gl v9. - // - // current: [0, 0, 0, 0, 0, 0, 1, 1, 1, 1] - // expected: [0, 0, 0, 0, 1, 1, 1, 1, 1, 1] + t.is(sizeTransition.buffers[0].byteLength, 4 * 4, 'buffer has correct size'); attributes.instanceSizes.setData({value: new Float32Array(10).fill(1)}); manager.update({attributes, transitions: {getSize: 1000}, numInstances: 10}); manager.run(); let transitioningBuffer = manager.getAttributes().instanceSizes.getBuffer(); let actual = await readArray(transitioningBuffer); - t.deepEquals(actual, [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], 'buffer is extended with new data'); + t.deepEquals(actual, [0, 0, 0, 0, 1, 1, 1, 1, 1, 1], 'buffer is extended with new data'); t.is(transitioningBuffer.byteLength, 10 * 4, 'buffer has correct size'); attributes.instanceSizes.setData({constant: true, value: [2]}); @@ -107,7 +97,7 @@ test('AttributeTransitionManager#update', async t => { manager.run(); transitioningBuffer = manager.getAttributes().instanceSizes.getBuffer(); actual = await readArray(transitioningBuffer); - t.deepEquals(actual, [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2], 'buffer is extended with new data'); + t.deepEquals(actual, [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2], 'buffer is extended with new data'); t.is(transitioningBuffer.byteLength, 12 * 4, 'buffer has correct size'); manager.finalize(); From 95b26cc4ee257a6444e97c97a64b4141c43e9ea2 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Wed, 13 Mar 2024 07:51:51 -0700 Subject: [PATCH 11/12] restore normalized warnings & explicitly set flag --- modules/core/src/lib/attribute/data-column.ts | 10 ++++++---- modules/core/src/transitions/gpu-transition-utils.ts | 10 ++++++---- modules/core/src/transitions/gpu-transition.ts | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/modules/core/src/lib/attribute/data-column.ts b/modules/core/src/lib/attribute/data-column.ts index 69631c18674..3e70792fefe 100644 --- a/modules/core/src/lib/attribute/data-column.ts +++ b/modules/core/src/lib/attribute/data-column.ts @@ -344,6 +344,8 @@ export default class DataColumn { constant?: boolean; value?: NumericArray; buffer?: Buffer; + /** Set to `true` if supplying float values to a unorm attribute */ + normalized?: boolean; } & Partial) ): boolean { const {state} = this; @@ -502,6 +504,7 @@ export default class DataColumn { if (!ArrayBuffer.isView(value)) { throw new Error(`Attribute ${this.id} value is not TypedArray`); } + const ArrayType = this.settings.defaultType; let illegalArrayType = false; if (this.doublePrecision) { @@ -511,10 +514,9 @@ export default class DataColumn { if (illegalArrayType) { throw new Error(`Attribute ${this.id} does not support ${value.constructor.name}`); } - // const ArrayType = this.settings.defaultType; - // if (!(value instanceof ArrayType) && this.settings.normalized && !('normalized' in opts)) { - // log.warn(`Attribute ${this.id} is normalized`)(); - // } + if (!(value instanceof ArrayType) && this.settings.normalized && !('normalized' in opts)) { + log.warn(`Attribute ${this.id} is normalized`)(); + } } // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer diff --git a/modules/core/src/transitions/gpu-transition-utils.ts b/modules/core/src/transitions/gpu-transition-utils.ts index 612fb09799d..73251991fc2 100644 --- a/modules/core/src/transitions/gpu-transition-utils.ts +++ b/modules/core/src/transitions/gpu-transition-utils.ts @@ -8,11 +8,13 @@ import {GL} from '@luma.gl/constants'; export function cloneAttribute(attribute: Attribute): Attribute { // `attribute.settings` is the original options passed when constructing the attribute. // This ensures that we set the proper `doublePrecision` flag and shader attributes. - const newAttribute = new Attribute(attribute.device, attribute.settings); + const {device, settings, value} = attribute; + const newAttribute = new Attribute(device, settings); // Placeholder value - necessary for generating the correct buffer layout - newAttribute.setData( - attribute.value instanceof Float64Array ? new Float64Array(0) : new Float32Array(0) - ); + newAttribute.setData({ + value: value instanceof Float64Array ? new Float64Array(0) : new Float32Array(0), + normalized: settings.normalized + }); return newAttribute; } diff --git a/modules/core/src/transitions/gpu-transition.ts b/modules/core/src/transitions/gpu-transition.ts index d6017c7d534..2fe85a80f5a 100644 --- a/modules/core/src/transitions/gpu-transition.ts +++ b/modules/core/src/transitions/gpu-transition.ts @@ -82,6 +82,7 @@ export abstract class GPUTransitionBase protected setBuffer(buffer: Buffer) { this.attributeInTransition.setData({ buffer, + normalized: this.attribute.settings.normalized, // Retain placeholder value to generate correct shader layout value: this.attributeInTransition.value as NumericArray }); From 22425c501d7d0d87889a7a91f95824bdfe052755 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 13 Mar 2024 11:08:35 -0400 Subject: [PATCH 12/12] use discard for buffer transform --- modules/core/src/transitions/gpu-interpolation-transition.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/core/src/transitions/gpu-interpolation-transition.ts b/modules/core/src/transitions/gpu-interpolation-transition.ts index cc3482d09ae..bdeffa30a98 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -94,10 +94,7 @@ export default class GPUInterpolationTransition extends GPUTransitionBase