@deck.gl/core
Version:
deck.gl core library
634 lines (559 loc) • 18.9 kB
text/typescript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
/* eslint-disable complexity */
import type {Device} from '@luma.gl/core';
import {Buffer, BufferLayout, BufferAttributeLayout, VertexType} from '@luma.gl/core';
import {
typedArrayFromDataType,
getBufferAttributeLayout,
getStride,
dataTypeFromTypedArray
} from './gl-utils';
import typedArrayManager from '../../utils/typed-array-manager';
import {toDoublePrecisionArray} from '../../utils/math-utils';
import log from '../../utils/log';
import type {TypedArray, NumericArray, TypedArrayConstructor} from '../../types/types';
export type DataType = Exclude<VertexType, 'float16'>;
export type LogicalDataType = DataType | 'float64';
export type BufferAccessor = {
/** Vertex data type. */
type?: DataType;
/** The number of elements per vertex attribute. */
size?: number;
/** Offset of the first vertex attribute into the buffer, in bytes. */
offset?: number;
/** The offset between the beginning of consecutive vertex attributes, in bytes. */
stride?: number;
};
export type ShaderAttributeOptions = Partial<BufferAccessor> & {
offset: number;
stride: number;
vertexOffset?: number;
elementOffset?: number;
};
function resolveShaderAttribute(
baseAccessor: DataColumnSettings<any>,
shaderAttributeOptions: Partial<ShaderAttributeOptions>
): ShaderAttributeOptions {
if (shaderAttributeOptions.offset) {
log.removed('shaderAttribute.offset', 'vertexOffset, elementOffset')();
}
// All shader attributes share the parent's stride
const stride = getStride(baseAccessor);
// `vertexOffset` is used to access the neighboring vertex's value
// e.g. `nextPositions` in polygon
const vertexOffset =
shaderAttributeOptions.vertexOffset !== undefined
? shaderAttributeOptions.vertexOffset
: baseAccessor.vertexOffset || 0;
// `elementOffset` is defined when shader attribute's size is smaller than the parent's
// e.g. `translations` in transform matrix
const elementOffset = shaderAttributeOptions.elementOffset || 0;
const offset =
// offsets defined by the attribute
vertexOffset * stride +
elementOffset * baseAccessor.bytesPerElement +
// offsets defined by external buffers if any
(baseAccessor.offset || 0);
return {
...shaderAttributeOptions,
offset,
stride
};
}
function resolveDoublePrecisionShaderAttributes(
baseAccessor: DataColumnSettings<any>,
shaderAttributeOptions: Partial<ShaderAttributeOptions>
): {
high: ShaderAttributeOptions;
low: ShaderAttributeOptions;
} {
const resolvedOptions = resolveShaderAttribute(baseAccessor, shaderAttributeOptions);
return {
high: resolvedOptions,
low: {
...resolvedOptions,
offset: resolvedOptions.offset + baseAccessor.size * 4
}
};
}
export type DataColumnOptions<Options> = Options &
Omit<BufferAccessor, 'type'> & {
id?: string;
vertexOffset?: number;
fp64?: boolean;
/** Vertex data type.
* @default 'float32'
*/
type?: LogicalDataType;
/** Internal API, use `type` instead */
logicalType?: LogicalDataType;
isIndexed?: boolean;
defaultValue?: number | number[];
};
export type DataColumnSettings<Options> = DataColumnOptions<Options> & {
type: DataType;
size: number;
logicalType?: LogicalDataType;
normalized: boolean;
bytesPerElement: number;
defaultValue: number[];
defaultType: TypedArrayConstructor;
};
type DataColumnInternalState<Options, State> = State & {
externalBuffer: Buffer | null;
bufferAccessor: DataColumnSettings<Options>;
allocatedValue: TypedArray | null;
numInstances: number;
bounds: [number[], number[]] | null;
constant: boolean;
};
export default class DataColumn<Options, State> {
device: Device;
id: string;
size: number;
settings: DataColumnSettings<Options>;
value: NumericArray | null;
doublePrecision: boolean;
protected _buffer: Buffer | null = null;
protected state: DataColumnInternalState<Options, State>;
/* eslint-disable max-statements */
constructor(device: Device, opts: DataColumnOptions<Options>, state: State) {
this.device = device;
this.id = opts.id || '';
this.size = opts.size || 1;
const logicalType = opts.logicalType || opts.type;
const doublePrecision = logicalType === 'float64';
let {defaultValue} = opts;
defaultValue = Number.isFinite(defaultValue)
? [defaultValue]
: defaultValue || new Array(this.size).fill(0);
let bufferType: DataType;
if (doublePrecision) {
bufferType = 'float32';
} else if (!logicalType && opts.isIndexed) {
bufferType = 'uint32';
} else {
bufferType = logicalType || 'float32';
}
// This is the attribute type defined by the layer
// If an external buffer is provided, this.type may be overwritten
// But we always want to use defaultType for allocation
let defaultType = typedArrayFromDataType(logicalType || bufferType);
this.doublePrecision = doublePrecision;
// `fp64: false` tells a double-precision attribute to allocate Float32Arrays
// by default when using auto-packing. This is more efficient in use cases where
// high precision is unnecessary, but the `64Low` attribute is still required
// by the shader.
if (doublePrecision && opts.fp64 === false) {
defaultType = Float32Array;
}
this.value = null;
this.settings = {
...opts,
defaultType,
defaultValue: defaultValue as number[],
logicalType,
type: bufferType,
normalized: bufferType.includes('norm'),
size: this.size,
bytesPerElement: defaultType.BYTES_PER_ELEMENT
};
this.state = {
...state,
externalBuffer: null,
bufferAccessor: this.settings,
allocatedValue: null,
numInstances: 0,
bounds: null,
constant: false
};
}
/* eslint-enable max-statements */
get isConstant(): boolean {
return this.state.constant;
}
get buffer(): Buffer {
return this._buffer!;
}
get byteOffset(): number {
const accessor = this.getAccessor();
if (accessor.vertexOffset) {
return accessor.vertexOffset * getStride(accessor);
}
return 0;
}
get numInstances(): number {
return this.state.numInstances;
}
set numInstances(n: number) {
this.state.numInstances = n;
}
delete(): void {
if (this._buffer) {
this._buffer.delete();
this._buffer = null;
}
typedArrayManager.release(this.state.allocatedValue);
}
getBuffer(): Buffer | null {
if (this.state.constant) {
return null;
}
return this.state.externalBuffer || this._buffer;
}
getValue(
attributeName: string = this.id,
options: Partial<ShaderAttributeOptions> | null = null
): Record<string, Buffer | TypedArray | null> {
const result: Record<string, Buffer | TypedArray | null> = {};
if (this.state.constant) {
const value = this.value as TypedArray;
if (options) {
const shaderAttributeDef = resolveShaderAttribute(this.getAccessor(), options);
const offset = shaderAttributeDef.offset / value.BYTES_PER_ELEMENT;
const size = shaderAttributeDef.size || this.size;
result[attributeName] = value.subarray(offset, offset + size);
} else {
result[attributeName] = value;
}
} else {
result[attributeName] = this.getBuffer();
}
if (this.doublePrecision) {
if (this.value instanceof Float64Array) {
result[`${attributeName}64Low`] = result[attributeName];
} else {
// Disable fp64 low part
result[`${attributeName}64Low`] = new Float32Array(this.size);
}
}
return result;
}
protected _getBufferLayout(
attributeName: string = this.id,
options: Partial<ShaderAttributeOptions> | null = null
): BufferLayout {
const accessor = this.getAccessor();
const attributes: BufferAttributeLayout[] = [];
const result: BufferLayout = {
name: this.id,
byteStride: getStride(accessor),
attributes
};
if (this.doublePrecision) {
const doubleShaderAttributeDefs = resolveDoublePrecisionShaderAttributes(
accessor,
options || {}
);
attributes.push(
getBufferAttributeLayout(
attributeName,
{...accessor, ...doubleShaderAttributeDefs.high},
this.device.type
),
getBufferAttributeLayout(
`${attributeName}64Low`,
{
...accessor,
...doubleShaderAttributeDefs.low
},
this.device.type
)
);
} else if (options) {
const shaderAttributeDef = resolveShaderAttribute(accessor, options);
attributes.push(
getBufferAttributeLayout(
attributeName,
{...accessor, ...shaderAttributeDef},
this.device.type
)
);
} else {
attributes.push(getBufferAttributeLayout(attributeName, accessor, this.device.type));
}
return result;
}
setAccessor(accessor: DataColumnSettings<Options>) {
this.state.bufferAccessor = accessor;
}
getAccessor(): DataColumnSettings<Options> {
return this.state.bufferAccessor;
}
// Returns [min: Array(size), max: Array(size)]
/* eslint-disable max-depth */
getBounds(): [number[], number[]] | null {
if (this.state.bounds) {
return this.state.bounds;
}
let result: [number[], number[]] | null = null;
if (this.state.constant && this.value) {
const min = Array.from(this.value);
result = [min, min];
} else {
const {value, numInstances, size} = this;
const len = numInstances * size;
if (value && len && value.length >= len) {
const min = new Array(size).fill(Infinity);
const max = new Array(size).fill(-Infinity);
for (let i = 0; i < len; ) {
for (let j = 0; j < size; j++) {
const v = value[i++];
if (v < min[j]) min[j] = v;
if (v > max[j]) max[j] = v;
}
}
result = [min, max];
}
}
this.state.bounds = result;
return result;
}
// returns true if success
// eslint-disable-next-line max-statements
setData(
data:
| TypedArray
| Buffer
| ({
constant?: boolean;
value?: NumericArray;
buffer?: Buffer;
/** Set to `true` if supplying float values to a unorm attribute */
normalized?: boolean;
} & Partial<BufferAccessor>)
): boolean {
const {state} = this;
let opts: {
constant?: boolean;
value?: NumericArray;
buffer?: Buffer;
} & Partial<BufferAccessor>;
if (ArrayBuffer.isView(data)) {
opts = {value: data};
} else if (data instanceof Buffer) {
opts = {buffer: data};
} else {
opts = data;
}
const accessor: DataColumnSettings<Options> = {...this.settings, ...opts};
if (ArrayBuffer.isView(opts.value)) {
if (!opts.type) {
// Deduce data type
const is64Bit = this.doublePrecision && opts.value instanceof Float64Array;
if (is64Bit) {
accessor.type = 'float32';
} else {
const type = dataTypeFromTypedArray(opts.value);
accessor.type = accessor.normalized ? (type.replace('int', 'norm') as DataType) : type;
}
}
accessor.bytesPerElement = opts.value.BYTES_PER_ELEMENT;
accessor.stride = getStride(accessor);
}
state.bounds = null; // clear cached bounds
if (opts.constant) {
// set constant
let value = opts.value;
value = this._normalizeValue(value, [], 0);
if (this.settings.normalized) {
value = this.normalizeConstant(value);
}
const hasChanged = !state.constant || !this._areValuesEqual(value, this.value);
if (!hasChanged) {
return false;
}
state.externalBuffer = null;
state.constant = true;
this.value = ArrayBuffer.isView(value) ? value : new Float32Array(value);
} else if (opts.buffer) {
const buffer = opts.buffer;
state.externalBuffer = buffer;
state.constant = false;
this.value = opts.value || null;
} else if (opts.value) {
this._checkExternalBuffer(opts);
let value = opts.value as TypedArray;
state.externalBuffer = null;
state.constant = false;
this.value = value;
let {buffer} = this;
const stride = getStride(accessor);
const byteOffset = (accessor.vertexOffset || 0) * stride;
if (this.doublePrecision && value instanceof Float64Array) {
value = toDoublePrecisionArray(value, accessor);
}
if (this.settings.isIndexed) {
const ArrayType = this.settings.defaultType;
if (value.constructor !== ArrayType) {
// Cast the index buffer to expected type
value = new ArrayType(value);
}
}
// A small over allocation is used as safety margin
// Shader attributes may try to access this buffer with bigger offsets
const requiredBufferSize = value.byteLength + byteOffset + stride * 2;
if (!buffer || buffer.byteLength < requiredBufferSize) {
buffer = this._createBuffer(requiredBufferSize);
}
buffer.write(value, byteOffset);
}
this.setAccessor(accessor);
return true;
}
updateSubBuffer(
opts: {
startOffset?: number;
endOffset?: number;
} = {}
): void {
this.state.bounds = null; // clear cached bounds
const value = this.value as TypedArray;
const {startOffset = 0, endOffset} = opts;
this.buffer.write(
this.doublePrecision && value instanceof Float64Array
? toDoublePrecisionArray(value, {
size: this.size,
startIndex: startOffset,
endIndex: endOffset
})
: value.subarray(startOffset, endOffset),
startOffset * value.BYTES_PER_ELEMENT + this.byteOffset
);
}
allocate(numInstances: number, copy: boolean = false): boolean {
const {state} = this;
const oldValue = state.allocatedValue;
// Allocate at least one element to ensure a valid buffer
const value = typedArrayManager.allocate(oldValue, numInstances + 1, {
size: this.size,
type: this.settings.defaultType,
copy
});
this.value = value;
const {byteOffset} = this;
let {buffer} = this;
if (!buffer || buffer.byteLength < value.byteLength + byteOffset) {
buffer = this._createBuffer(value.byteLength + byteOffset);
if (copy && oldValue) {
// Upload the full existing attribute value to the GPU, so that updateBuffer
// can choose to only update a partial range.
// TODO - copy old buffer to new buffer on the GPU
buffer.write(
oldValue instanceof Float64Array ? toDoublePrecisionArray(oldValue, this) : oldValue,
byteOffset
);
}
}
state.allocatedValue = value;
state.constant = false;
state.externalBuffer = null;
this.setAccessor(this.settings);
return true;
}
// PRIVATE HELPER METHODS
protected _checkExternalBuffer(opts: {value?: NumericArray; normalized?: boolean}): void {
const {value} = opts;
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) {
// not 32bit or 64bit
illegalArrayType = value.BYTES_PER_ELEMENT < 4;
}
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`)();
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
normalizeConstant(value: NumericArray): NumericArray {
/* eslint-disable complexity */
switch (this.settings.type) {
case 'snorm8':
// normalize [-128, 127] to [-1, 1]
return new Float32Array(value).map(x => ((x + 128) / 255) * 2 - 1);
case 'snorm16':
// normalize [-32768, 32767] to [-1, 1]
return new Float32Array(value).map(x => ((x + 32768) / 65535) * 2 - 1);
case 'unorm8':
// normalize [0, 255] to [0, 1]
return new Float32Array(value).map(x => x / 255);
case 'unorm16':
// normalize [0, 65535] to [0, 1]
return new Float32Array(value).map(x => x / 65535);
default:
// No normalization for gl.FLOAT and gl.HALF_FLOAT
return value;
}
}
/* check user supplied values and apply fallback */
protected _normalizeValue(value: any, out: NumericArray, start: number): NumericArray {
const {defaultValue, size} = this.settings;
if (Number.isFinite(value)) {
out[start] = value;
return out;
}
if (!value) {
let i = size;
while (--i >= 0) {
out[start + i] = defaultValue[i];
}
return out;
}
// Important - switch cases are 5x more performant than a for loop!
/* eslint-disable no-fallthrough, default-case */
switch (size) {
case 4:
out[start + 3] = Number.isFinite(value[3]) ? value[3] : defaultValue[3];
case 3:
out[start + 2] = Number.isFinite(value[2]) ? value[2] : defaultValue[2];
case 2:
out[start + 1] = Number.isFinite(value[1]) ? value[1] : defaultValue[1];
case 1:
out[start + 0] = Number.isFinite(value[0]) ? value[0] : defaultValue[0];
break;
default:
// In the rare case where the attribute size > 4, do it the slow way
// This is used for e.g. transform matrices
let i = size;
while (--i >= 0) {
out[start + i] = Number.isFinite(value[i]) ? value[i] : defaultValue[i];
}
}
return out;
}
protected _areValuesEqual(value1: any, value2: any): boolean {
if (!value1 || !value2) {
return false;
}
const {size} = this;
for (let i = 0; i < size; i++) {
if (value1[i] !== value2[i]) {
return false;
}
}
return true;
}
protected _createBuffer(byteLength: number): Buffer {
if (this._buffer) {
this._buffer.destroy();
}
const {isIndexed, type} = this.settings;
this._buffer = this.device.createBuffer({
...this._buffer?.props,
id: this.id,
// TODO(ibgreen) - WebGPU requires COPY_DST and COPY_SRC to allow write / read
usage: (isIndexed ? Buffer.INDEX : Buffer.VERTEX) | Buffer.COPY_DST,
indexType: isIndexed ? (type as 'uint16' | 'uint32') : undefined,
byteLength
});
return this._buffer;
}
}