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..3e32b0817ae 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 './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-transition-utils.ts b/modules/core/src/lib/attribute/attribute-transition-utils.ts deleted file mode 100644 index ebc7172fffe..00000000000 --- a/modules/core/src/lib/attribute/attribute-transition-utils.ts +++ /dev/null @@ -1,216 +0,0 @@ -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 - }; -} - -// 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; -} - -/** Returns the GLSL attribute type for the given number of float32 components. */ -export function getAttributeTypeFromSize(size: number): string { - switch (size) { - case 1: - return 'float'; - case 2: - return 'vec2'; - case 3: - return 'vec3'; - case 4: - return 'vec4'; - default: - throw new Error(`No defined attribute type for size "${size}"`); - } -} - -/** Returns the {@link VertexFormat} for the given number of float32 components. */ -export function getFloat32VertexFormat(size: 1 | 2 | 3 | 4): LumaVertexFormat { - switch (size) { - case 1: - return 'float32'; - case 2: - return 'float32x2'; - case 3: - return 'float32x3'; - case 4: - return 'float32x4'; - default: - throw new Error('invalid type size'); - } -} - -export function cycleBuffers(buffers: Buffer[]): void { - buffers.push(buffers.shift() as Buffer); -} - -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; -} - -// 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 -// buffer layout of [3, 4] might have data [A1, A2, A3, B1, B2, B3, B4]. If it needs to transition -// to a buffer layout of [4, 2], it should produce a buffer, using the transition setting's `enter` -// function, that looks like this: [A1, A2, A3, A4 (user `enter` fn), B1, B2, 0]. Note: the final -// 0 in this buffer is because we never shrink buffers, only grow them, for performance reasons. -// -// 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({ - buffer, - numInstances, - attribute, - fromLength, - fromStartIndices, - getData = x => x -}: { - buffer: Buffer; - numInstances: number; - attribute: Attribute; - fromLength: number; - fromStartIndices?: NumericArray | null; - getData?: (toValue: NumericArray, chunk?: NumericArray) => NumericArray; -}): Buffer { - // TODO: move the precisionMultiplier logic to the attribute when retrieving - // its `size` and `elementOffset`? - const precisionMultiplier = - attribute.doublePrecision && attribute.value instanceof Float64Array ? 2 : 1; - const size = attribute.size * precisionMultiplier; - const byteOffset = attribute.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 - if (!hasStartIndices && fromLength >= toLength) { - return buffer; - } - - const toData = isConstant - ? (attribute.value as TypedArray) - : // TODO(v9.1): Avoid non-portable synchronous reads. - toFloat32Array(attribute.getBuffer()!.readSyncWebGL()); - 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); - - // TODO(v9.1): Avoid non-portable synchronous reads. - const source = toFloat32Array(buffer.readSyncWebGL()); - const target = new Float32Array(toLength); - padArray({ - source, - target, - sourceStartIndices: fromStartIndices, - targetStartIndices: toStartIndices, - size, - getData: getMissingData - }); - - if (buffer.byteLength < target.byteLength + byteOffset) { - buffer = buffer.device.createBuffer({byteLength: target.byteLength + byteOffset}); - } - buffer.write(target, byteOffset); - 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/lib/attribute/attribute.ts b/modules/core/src/lib/attribute/attribute.ts index 8b663199e76..9199cd42a5e 100644 --- a/modules/core/src/lib/attribute/attribute.ts +++ b/modules/core/src/lib/attribute/attribute.ts @@ -10,7 +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 './attribute-transition-utils'; +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/lib/attribute/data-column.ts b/modules/core/src/lib/attribute/data-column.ts index a23c8c39638..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; diff --git a/modules/core/src/lib/attribute/transition-settings.ts b/modules/core/src/lib/attribute/transition-settings.ts new file mode 100644 index 00000000000..51daa25c02f --- /dev/null +++ b/modules/core/src/lib/attribute/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/lib/uniform-transition-manager.ts b/modules/core/src/lib/uniform-transition-manager.ts index d32c4472d98..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 './attribute/attribute-transition-utils'; +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 d51ff9bb99e..bdeffa30a98 100644 --- a/modules/core/src/transitions/gpu-interpolation-transition.ts +++ b/modules/core/src/transitions/gpu-interpolation-transition.ts @@ -1,33 +1,22 @@ import type {Device} 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, - getAttributeBufferLength, cycleBuffers, - InterpolationTransitionSettings, padBuffer, + matchBuffer, getFloat32VertexFormat -} from '../lib/attribute/attribute-transition-utils'; -import Transition from './transition'; +} from './gpu-transition-utils'; +import {GPUTransitionBase} from './gpu-transition'; -import type {NumericArray} 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, @@ -38,48 +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.userData` 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); - } - 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); - const bufferOpts = { - byteLength: attribute.buffer.byteLength, - usage: GL.DYNAMIC_COPY - }; - this.buffers = [ - device.createBuffer(bufferOpts), // from - device.createBuffer(bufferOpts) // current - ]; } - 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. @@ -87,71 +48,58 @@ export default class GPUInterpolationTransition implements GPUTransition { // And the other buffer is now the current buffer. cycleBuffers(buffers); - const padBufferOpts = { - numInstances, + buffers[0] = padBuffer({ + device: this.device, + buffer: buffers[0], attribute, - fromLength: this.currentLength, - fromStartIndices: this.currentStartIndices, + fromLength: prevLength, + toLength: this.currentLength, + fromStartIndices: prevStartIndices, 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)` - ); - } - } - - this.currentStartIndices = attribute.startIndices; - this.currentLength = getAttributeBufferLength(attribute, numInstances); - this.attributeInTransition.setData({ - buffer: buffers[1], - // Hack: Float64Array is required for double-precision attributes - // to generate correct shader attributes - value: attribute.value as NumericArray + }); + buffers[1] = matchBuffer({ + device: this.device, + source: buffers[0], + target: buffers[1] }); - 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]} - ); - this.transform.transformFeedback.setBuffers({vCurrent: buffers[1]}); + this.setBuffer(buffers[1]); + + 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}); + } else { + model.setAttributes({ + aFrom: buffers[0], + aTo: attribute.getBuffer()! + }); + } + transform.transformFeedback.setBuffers({vCurrent: buffers[1]}); } - update(): boolean { - const updated = this.transition.update(); - if (updated) { - const {duration, easing} = this.settings as InterpolationTransitionSettings; - const {time} = this.transition; - let t = time / duration; - if (easing) { - t = easing(t); - } - this.transform.model.setUniforms({time: t}); - 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({discard: true}); } - cancel(): void { - this.transition.cancel(); - this.transform.delete(); - for (const buffer of this.buffers) { - buffer.delete(); - } - this.buffers.length = 0; + override delete() { + super.delete(); + this.transform.destroy(); } } @@ -172,12 +120,11 @@ 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, { 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..825d48e89da 100644 --- a/modules/core/src/transitions/gpu-spring-transition.ts +++ b/modules/core/src/transitions/gpu-spring-transition.ts @@ -1,42 +1,24 @@ -/* 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, Framebuffer, Texture} from '@luma.gl/core'; +import {Timeline, BufferTransform} from '@luma.gl/engine'; import { padBuffer, + matchBuffer, 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 {GPUTransitionBase} from './gpu-transition'; -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 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: LumaTexture2D; - private framebuffer: LumaFramebuffer; - private transform: LumaTransform; - private buffers: [LumaBuffer, LumaBuffer, LumaBuffer]; + private texture: Texture; + private framebuffer: Framebuffer; + private transform: BufferTransform; constructor({ device, @@ -47,99 +29,57 @@ export default class GPUSpringTransition implements GPUTransition { attribute: Attribute; 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. - // This ensures that we set the proper `doublePrecision` flag and shader attributes. - this.attributeInTransition = new Attribute(device, attribute.settings); - 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); - const bufferOpts = { - byteLength: 0, - usage: GL.DYNAMIC_COPY - }; - this.buffers = [ - device.createBuffer(bufferOpts), // previous - device.createBuffer(bufferOpts), // current - device.createBuffer(bufferOpts) // next - ]; - this.transform = getTransform(device, attribute, this.buffers); + 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 { 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)` - ); - } + for (let i = 0; i < 2; i++) { + buffers[i] = padBuffer({ + device: this.device, + buffer: buffers[i], + attribute, + fromLength: prevLength, + toLength: this.currentLength, + fromStartIndices: prevStartIndices, + getData: transitionSettings.enter + }); } - - this.settings = transitionSettings; - this.currentStartIndices = attribute.startIndices; - this.currentLength = getAttributeBufferLength(attribute, numInstances); - this.attributeInTransition.setData({ - buffer: buffers[1], - // Hack: Float64Array is required for double-precision attributes - // to generate correct shader attributes - value: attribute.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]); - 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(this.currentLength / attribute.size)); + if (attribute.isConstant) { + model.setConstantAttributes({aTo: attribute.value as TypedArray}); + } else { + model.setAttributes({aTo: attribute.getBuffer()!}); + } } - update() { + onUpdate() { const {buffers, transform, framebuffer, transition} = this; - const updated = transition.update(); - if (!updated) { - return false; - } + const settings = this.settings as SpringTransitionSettings; - this.transform.model.setAttributes({aPrev: buffers[0], aCur: buffers[1]}); - this.transform.transformFeedback.setBuffers({vNext: buffers[2]}); + transform.model.setAttributes({ + aPrev: buffers[0], + aCur: buffers[1] + }); + transform.transformFeedback.setBuffers({vNext: buffers[2]}); transform.model.setUniforms({ stiffness: settings.stiffness, damping: settings.damping @@ -152,31 +92,20 @@ 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 - }); + this.setBuffer(buffers[1]); const isTransitioning = this.device.readPixelsToArrayWebGL(framebuffer)[0] > 0; if (!isTransitioning) { transition.end(); } - - return true; } - cancel() { - this.transition.cancel(); - this.transform.delete(); - for (const buffer of this.buffers) { - buffer.delete(); - } - (this.buffers as LumaBuffer[]).length = 0; - this.texture.delete(); - this.framebuffer.delete(); + override delete() { + super.delete(); + this.transform.destroy(); + this.texture.destroy(); + this.framebuffer.destroy(); } } @@ -227,23 +156,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 +181,7 @@ function getTransform( }); } -function getTexture(device: Device): LumaTexture2D { +function getTexture(device: Device): Texture { return device.createTexture({ data: new Uint8Array(4), format: 'rgba8unorm', @@ -269,7 +192,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/modules/core/src/transitions/gpu-transition-utils.ts b/modules/core/src/transitions/gpu-transition-utils.ts new file mode 100644 index 00000000000..73251991fc2 --- /dev/null +++ b/modules/core/src/transitions/gpu-transition-utils.ts @@ -0,0 +1,186 @@ +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 {GL} from '@luma.gl/constants'; + +/** 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 {device, settings, value} = attribute; + const newAttribute = new Attribute(device, settings); + // Placeholder value - necessary for generating the correct buffer layout + newAttribute.setData({ + value: value instanceof Float64Array ? new Float64Array(0) : new Float32Array(0), + normalized: settings.normalized + }); + return newAttribute; +} + +/** Returns the GLSL attribute type for the given number of float32 components. */ +export function getAttributeTypeFromSize(size: number): string { + switch (size) { + case 1: + return 'float'; + case 2: + return 'vec2'; + case 3: + return 'vec3'; + case 4: + return 'vec4'; + default: + throw new Error(`No defined attribute type for size "${size}"`); + } +} + +/** Returns the {@link VertexFormat} for the given number of float32 components. */ +export function getFloat32VertexFormat(size: number): VertexFormat { + switch (size) { + case 1: + return 'float32'; + case 2: + return 'float32x2'; + case 3: + return 'float32x3'; + case 4: + return 'float32x4'; + default: + throw new Error('invalid type size'); + } +} + +export function cycleBuffers(buffers: Buffer[]): void { + buffers.push(buffers.shift() as Buffer); +} + +export function getAttributeBufferLength(attribute: Attribute, numInstances: number): number { + const {doublePrecision, settings, value, size} = attribute; + const multiplier = doublePrecision && value instanceof Float64Array ? 2 : 1; + 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 + ); +} + +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 +// within a buffer should be grouped for drawElements, like the Polygon layer.) For example, a +// buffer layout of [3, 4] might have data [A1, A2, A3, B1, B2, B3, B4]. If it needs to transition +// to a buffer layout of [4, 2], it should produce a buffer, using the transition setting's `enter` +// function, that looks like this: [A1, A2, A3, A4 (user `enter` fn), B1, B2, 0]. Note: the final +// 0 in this buffer is because we never shrink buffers, only grow them, for performance reasons. +// +// 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, + toLength, + fromStartIndices, + getData = x => x +}: { + device: Device; + buffer?: Buffer; + attribute: Attribute; + fromLength: number; + toLength: number; + fromStartIndices?: NumericArray | null; + getData?: (toValue: NumericArray, chunk?: NumericArray) => NumericArray; +}): Buffer { + // TODO: move the precisionMultiplier logic to the attribute when retrieving + // its `size` and `elementOffset`? + const precisionMultiplier = + 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 + : byteOffset; + const toStartIndices = attribute.startIndices; + const hasStartIndices = fromStartIndices && toStartIndices; + const isConstant = attribute.isConstant; + + // check if buffer needs to be padded + if (!hasStartIndices && buffer && fromLength >= toLength) { + 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. + 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: 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 = buffer + ? new Float32Array(buffer.readSyncWebGL(targetByteOffset, fromLength * 4).buffer) + : new Float32Array(0); + const target = new Float32Array(toLength); + padArray({ + source, + target, + sourceStartIndices: fromStartIndices, + targetStartIndices: toStartIndices, + size, + getData: getMissingData + }); + + if (!buffer || buffer.byteLength < target.byteLength + targetByteOffset) { + buffer?.destroy(); + buffer = device.createBuffer({ + byteLength: target.byteLength + targetByteOffset, + usage: GL.DYNAMIC_COPY + }); + } + buffer.write(target, targetByteOffset); + return buffer; +} diff --git a/modules/core/src/transitions/gpu-transition.ts b/modules/core/src/transitions/gpu-transition.ts index 450daf3efea..2fe85a80f5a 100644 --- a/modules/core/src/transitions/gpu-transition.ts +++ b/modules/core/src/transitions/gpu-transition.ts @@ -1,12 +1,102 @@ +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 '../lib/attribute/attribute-transition-utils'; +import type {TransitionSettings} from '../lib/attribute/transition-settings'; +import type {NumericArray} from '../types/types'; -export default interface GPUTransition { +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, + normalized: this.attribute.settings.normalized, + // 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; + } } 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, 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" }, 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(); 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