From c6da5cf8d74c379b0897b2d81f661620ba4cd159 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sat, 11 May 2024 20:00:13 -0700 Subject: [PATCH 1/6] GPUAggregator --- .../gpu-aggregator/gpu-aggregator.ts | 246 ++++++++++++++++++ .../gpu-aggregator/utils.ts | 29 +++ .../webgl-aggregation-transform.ts | 227 ++++++++++++++++ .../gpu-aggregator/webgl-bin-sorter.ts | 246 ++++++++++++++++++ 4 files changed, 748 insertions(+) create mode 100644 modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts create mode 100644 modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/utils.ts create mode 100644 modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts create mode 100644 modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts new file mode 100644 index 00000000000..7849d80af51 --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts @@ -0,0 +1,246 @@ +import {ModelProps} from '@luma.gl/engine'; +import {WebGLBinSorter} from './webgl-bin-sorter'; +import {WebGLAggregationTransform} from './webgl-aggregation-transform'; +import {_deepEqual as deepEqual, BinaryAttribute} from '@deck.gl/core'; + +import type {Aggregator, AggregationProps} from '../aggregator'; +import type {Device, Buffer, BufferLayout, TypedArray} from '@luma.gl/core'; +import type {ShaderModule} from '@luma.gl/shadertools'; + +/** Settings used to construct a new GPUAggregator */ +export type GPUAggregatorSettings = { + /** Size of bin IDs */ + dimensions: 1 | 2; + /** How many properties to perform aggregation on */ + numChannels: 1 | 2 | 3; + /** Buffer layout for input attributes */ + bufferLayout?: BufferLayout[]; + /** Define a shader function with one of the signatures + * `void getBin(out int binId)`: if binCount is a number + * `void getBin(out ivec2 binId)`: if binCount is a 2D array + * And a shader function with one of the signatures + * `void getWeight(out float weight)`: if numChannels=1 + * `void getWeight(out vec2 weight)`: if numChannels=2 + * `void getWeight(out vec3 weight)`: if numChannels=3 + */ + vs: string; + /** Shader modules + * Required to support certain layer extensions (e.g. data filter) + */ + modules?: ShaderModule[]; + /** Shadertool module defines */ + defines?: Record; +}; + +/** Options used to run GPU aggregation, can be changed at any time */ +export type GPUAggregationProps = AggregationProps & { + /** Number of bins, can be either 1D or 2D. + * - 1D: vertex shader should implement a getBin function that yields an int id. + * Only ids within the range of [0, binCount] are counted. + * - 2D: vertex shader should implement a getBin function that yields an ivec2 id. + * Only ids within the range of [[0, 0], binCount] are counted. + */ + binCount: number | [number, number]; + /** Context props passed to the shader modules */ + moduleSettings?: ModelProps['moduleSettings']; +}; + +/** An Aggregator implementation that calculates aggregation on the GPU */ +export class GPUAggregator implements Aggregator { + /** Checks if the current device supports GPU aggregation */ + static isSupported(device: Device): boolean { + return ( + device.features.has('float32-renderable-webgl') && + device.features.has('texture-blend-float-webgl') + ); + } + + dimensions: 1 | 2; + numChannels: 1 | 2 | 3; + numBins: number = 0; + + device: Device; + props: GPUAggregationProps = { + pointCount: 0, + binCount: 0, + operations: [], + attributes: {}, + binOptions: {} + }; + + /** Dirty flag per channel */ + protected needsUpdate: boolean[]; + /** Step 1. sort data points into bins, blended using an aggregation opera†ion */ + protected binSorter: WebGLBinSorter; + /** Step 2. (optional) calculate the min/max across all bins */ + protected aggregationTransform: WebGLAggregationTransform; + + constructor(device: Device, settings: GPUAggregatorSettings) { + this.device = device; + this.dimensions = settings.dimensions; + this.numChannels = settings.numChannels; + this.needsUpdate = new Array(this.numChannels).fill(true); + this.binSorter = new WebGLBinSorter(device, settings); + this.aggregationTransform = new WebGLAggregationTransform(device, settings); + } + + getBins(): BinaryAttribute | null { + const buffer = this.aggregationTransform.binBuffer; + if (!buffer) { + return null; + } + return {buffer, type: 'float32', size: this.dimensions}; + } + + /** Returns an accessor to the output for a given channel. */ + getResult(channel: 0 | 1 | 2): BinaryAttribute | null { + const buffer = this.aggregationTransform.valueBuffer; + if (!buffer || channel >= this.numChannels) { + return null; + } + return {buffer, type: 'float32', size: 1, stride: this.numChannels * 4, offset: channel * 4}; + } + + /** Returns the [min, max] of aggregated values for a given channel. */ + getResultDomain(channel: 0 | 1 | 2): [min: number, max: number] { + return this.aggregationTransform.domains[channel]; + } + + /** Returns the information for a given bin. */ + getBin(index: number): { + /** The original id */ + id: number | [number, number]; + /** Aggregated values by channel */ + value: number[]; + /** Count of data points in this bin */ + count: number; + } | null { + if (index < 0 || index >= this.numBins) { + return null; + } + const {binCount} = this.props; + const id = Array.isArray(binCount) + ? ([index % binCount[0], Math.floor(index / binCount[0])] as [number, number]) + : index; + + const pixel = this.binSorter.getBinValues(index); + if (!pixel) { + return null; + } + const count = pixel[3]; + const value: number[] = []; + for (let channel = 0; channel < this.numChannels; channel++) { + if (count === 0) { + value[channel] = NaN; + } else { + value[channel] = + this.props.operations[channel] === 'MEAN' ? pixel[channel] / count : pixel[channel]; + } + } + return {id, value, count}; + } + + /** Release GPU resources */ + destroy() { + this.binSorter.destroy(); + this.aggregationTransform.destroy(); + } + + /** Update aggregation props. Normalize prop values and set change flags. */ + setProps(props: Partial) { + const oldProps = this.props; + + // Update local settings. These will set the flag this._needsUpdate + if ('binCount' in props && !deepEqual(props.binCount, oldProps.binCount, 1)) { + const binCount = props.binCount!; + this.numBins = Array.isArray(binCount) ? binCount[0] * binCount[1] : binCount; + + this.binSorter.setDimensions(this.numBins, binCount[0]); + this.aggregationTransform.setDimensions(this.numBins, binCount[0]); + this.setNeedsUpdate(); + } + if (props.operations) { + for (let channel = 0; channel < this.numChannels; channel++) { + if (props.operations[channel] !== oldProps.operations[channel]) { + this.setNeedsUpdate(channel); + } + } + } + if (props.pointCount !== undefined && props.pointCount !== oldProps.pointCount) { + this.binSorter.setModelProps({vertexCount: props.pointCount}); + this.setNeedsUpdate(); + } + if (props.binOptions) { + if (!deepEqual(props.binOptions, oldProps.binOptions, 2)) { + this.setNeedsUpdate(); + } + this.binSorter.setModelProps({uniforms: props.binOptions}); + } + if (props.attributes) { + const attributeBuffers: Record = {}; + const constantAttributes: Record = {}; + + for (const attribute of Object.values(props.attributes)) { + for (const [attributeName, value] of Object.entries(attribute.getValue())) { + if (ArrayBuffer.isView(value)) { + constantAttributes[attributeName] = value; + } else if (value) { + attributeBuffers[attributeName] = value; + } + } + } + this.binSorter.setModelProps({attributes: attributeBuffers, constantAttributes}); + } + if (props.moduleSettings) { + this.binSorter.setModelProps({moduleSettings: props.moduleSettings}); + } + + Object.assign(this.props, props); + } + + /** Flags a channel to need update. + * This is called internally by setProps() if certain props change + * Users of this class still need to manually set the dirty flag sometimes, because even if no props changed + * the underlying buffers could have been updated and require rerunning the aggregation + * @param {number} channel - mark the given channel as dirty. If not provided, all channels will be updated. + */ + setNeedsUpdate(channel?: number) { + if (channel === undefined) { + this.needsUpdate.fill(true); + } else { + this.needsUpdate[channel] = true; + } + } + + /** Run aggregation */ + update( + /** Parameters only available at runtime */ + opts: { + moduleSettings?: ModelProps['moduleSettings']; + } + ) { + if (!this.needsUpdate.some(Boolean)) { + return; + } + + if (opts.moduleSettings) { + this.binSorter.setModelProps(opts); + } + + const {operations} = this.props; + const operationsToUpdate = this.needsUpdate.map((needsUpdate, i) => + needsUpdate ? operations[i] : null + ); + // Render data to bins + this.binSorter.update(operationsToUpdate); + // Read to buffer and calculate domain + this.aggregationTransform.update(this.binSorter.texture!, operations); + + this.needsUpdate.fill(false); + + // Uncomment to debug + // console.log('binsFBO', new Float32Array(this.device.readPixelsToArrayWebGL(this.binSorter.texture!).buffer)); + // console.log('binsBuffer', new Float32Array(this.aggregationTransform.binBuffer?.readSyncWebGL().buffer!)); + // console.log('resultBuffer', new Float32Array(this.aggregationTransform.valueBuffer?.readSyncWebGL().buffer!)); + } +} diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/utils.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/utils.ts new file mode 100644 index 00000000000..51d34d7cb48 --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/utils.ts @@ -0,0 +1,29 @@ +import type {Device, Framebuffer} from '@luma.gl/core'; + +/** + * Marks GLSL shaders for syntax highlighting: glsl`...` + * Install https://marketplace.visualstudio.com/items?itemName=boyswan.glsl-literal + */ +export const glsl = (s: TemplateStringsArray) => `${s}`; + +/** + * Create a float texture to store aggregation result + */ +export function createRenderTarget(device: Device, width: number, height: number): Framebuffer { + return device.createFramebuffer({ + width, + height, + colorAttachments: [ + device.createTexture({ + width, + height, + format: 'rgba32float', + mipmaps: false, + sampler: { + minFilter: 'nearest', + magFilter: 'nearest' + } + }) + ] + }); +} diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts new file mode 100644 index 00000000000..5ebf0f0429a --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts @@ -0,0 +1,227 @@ +import {BufferTransform} from '@luma.gl/engine'; +import {glsl, createRenderTarget} from './utils'; + +import type {Device, Framebuffer, Buffer, Texture} from '@luma.gl/core'; +import type {GPUAggregatorSettings} from './gpu-aggregator'; +import type {AggregationOperation} from '../aggregator'; + +import {TEXTURE_WIDTH} from './webgl-bin-sorter'; + +const MAX_FLOAT32 = 3e38; + +export class WebGLAggregationTransform { + device: Device; + numChannels: number; + + /** Packed from bin ids */ + binBuffer: Buffer | null = null; + /** Packed values from each channel of each bin + * Stride is number of channels * 4 bytes (float32) + */ + valueBuffer: Buffer | null = null; + + private transform: BufferTransform; + /** Render target for calculating domain */ + private domainFBO: Framebuffer; + /** Aggregated [min, max] for each channel */ + private _domains: [min: number, max: number][] | null = null; + + constructor(device: Device, settings: GPUAggregatorSettings) { + this.device = device; + this.numChannels = settings.numChannels; + this.transform = createTransform(device, settings); + this.domainFBO = createRenderTarget(device, 2, 1); + } + + destroy() { + this.transform.destroy(); + this.binBuffer?.destroy(); + this.valueBuffer?.destroy(); + this.domainFBO.colorAttachments[0].texture.destroy(); + this.domainFBO.destroy(); + } + + get domains(): [min: number, max: number][] { + if (!this._domains) { + // Domain model has run, but result has not been read to CPU + const buffer = this.device.readPixelsToArrayWebGL(this.domainFBO).buffer; + const domain = new Float32Array(buffer); + this._domains = [ + [-domain[4], domain[0]], + [-domain[5], domain[1]], + [-domain[6], domain[2]] + ].slice(0, this.numChannels) as [number, number][]; + } + return this._domains; + } + + setDimensions(numBins: number, stride: number = 0) { + const {model, transformFeedback} = this.transform; + model.setVertexCount(numBins); + model.setUniforms({ + binStride: stride + }); + + // Only destroy existing buffer if it is not large enough + const binBufferByteLength = numBins * (stride ? 2 : 1) * 4; + if (!this.binBuffer || this.binBuffer.byteLength < binBufferByteLength) { + this.binBuffer?.destroy(); + this.binBuffer = this.device.createBuffer({byteLength: binBufferByteLength}); + transformFeedback.setBuffer('binIds', this.binBuffer); + } + + const valueBufferByteLength = numBins * this.numChannels * 4; + if (!this.valueBuffer || this.valueBuffer.byteLength < valueBufferByteLength) { + this.valueBuffer?.destroy(); + this.valueBuffer = this.device.createBuffer({byteLength: valueBufferByteLength}); + transformFeedback.setBuffer('values', this.valueBuffer); + } + } + + update(bins: Texture, operations: AggregationOperation[]) { + const transform = this.transform; + const target = this.domainFBO; + + transform.model.setUniforms({ + isMean: Array.from({length: 3}, (_, i) => (operations[i] === 'MEAN' ? 1 : 0)) + }); + transform.model.setBindings({bins}); + + transform.run({ + id: 'gpu-aggregation-domain', + framebuffer: target, + parameters: { + viewport: [0, 0, 2, 1] + }, + clearColor: [-MAX_FLOAT32, -MAX_FLOAT32, -MAX_FLOAT32, 0], + clearDepth: false, + clearStencil: false + }); + + // Clear the last read results. This will be lazy-populated if used. + this._domains = null; + } +} + +function createTransform(device: Device, settings: GPUAggregatorSettings): BufferTransform { + const vs = glsl`\ +#version 300 es +#define SHADER_NAME gpu-aggregation-domain-vertex + +uniform int binStride; +uniform bvec3 isMean; +uniform float naN; +uniform sampler2D bins; + +#if NUM_DIMS == 1 +out float binIds; +#else +out vec2 binIds; +#endif + +#if NUM_CHANNELS == 1 +out float values; +#elif NUM_CHANNELS == 2 +out vec2 values; +#else +out vec3 values; +#endif + +void main() { + int row = gl_VertexID / SAMEPLER_WIDTH; + int col = gl_VertexID - row * SAMEPLER_WIDTH; + vec4 weights = texelFetch(bins, ivec2(col, row), 0); + vec3 value3 = mix(weights.rgb, weights.rgb / max(weights.a, 1.0), isMean); + if (weights.a == 0.0) { + value3 = vec3(naN); + } + +#if NUM_DIMS == 1 + binIds = float(gl_VertexID); +#else + int y = gl_VertexID / binStride; + int x = gl_VertexID - y * binStride; + binIds.y = float(y); + binIds.x = float(x); +#endif + +#if NUM_CHANNELS == 3 + values = value3; +#elif NUM_CHANNELS == 2 + values = value3.xy; +#else + values = value3.x; +#endif + + gl_Position = vec4(0., 0., 0., 1.); + gl_PointSize = 2.0; +} +`; + + const fs = glsl`\ +#version 300 es +#define SHADER_NAME gpu-aggregation-domain-fragment + +precision highp float; + +#if NUM_CHANNELS == 1 +in float values; +#elif NUM_CHANNELS == 2 +in vec2 values; +#else +in vec3 values; +#endif + +out vec4 fragColor; + +void main() { +#if NUM_CHANNELS > 1 + if (isnan(values.x)) discard; +#else + if (isnan(values)) discard; +#endif + vec3 value3; +#if NUM_CHANNELS == 3 + value3 = values; +#elif NUM_CHANNELS == 2 + value3.xy = values; +#else + value3.x = values; +#endif + // This shader renders into a 2x1 texture with blending=max + // The left pixel yields the max value of each channel + // The right pixel yields the min value of each channel + if (gl_FragCoord.x < 1.0) { + fragColor = vec4(value3, 1.0); + } else { + fragColor = vec4(-value3, 1.0); + } +} +`; + + return new BufferTransform(device, { + vs, + fs, + topology: 'point-list', + parameters: { + blendColorSrcFactor: 'one', + blendColorDstFactor: 'one', + blendColorOperation: 'max', + blendAlphaSrcFactor: 'one', + blendAlphaDstFactor: 'one', + blendAlphaOperation: 'max' + }, + defines: { + NUM_DIMS: settings.dimensions, + NUM_CHANNELS: settings.numChannels, + SAMEPLER_WIDTH: TEXTURE_WIDTH + }, + uniforms: { + // Passed in as uniform because 1) there is no GLSL symbol for NaN 2) any expression that exploits undefined behavior to produces NaN + // will subject to platform differences and shader optimization + naN: NaN + }, + varyings: ['binIds', 'values'], + disableWarnings: true + }); +} diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts new file mode 100644 index 00000000000..6ec800aa09c --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts @@ -0,0 +1,246 @@ +import {Model, ModelProps} from '@luma.gl/engine'; +import {glsl, createRenderTarget} from './utils'; + +import type {Device, Framebuffer, Texture} from '@luma.gl/core'; +import type {GPUAggregatorSettings} from './gpu-aggregator'; +import type {AggregationOperation} from '../aggregator'; + +const COLOR_CHANNELS = [0x1, 0x2, 0x4, 0x8]; // GPU color mask RED, GREEN, BLUE, ALPHA +const MAX_FLOAT32 = 3e38; +const EMPTY_MASKS = {SUM: 0, MEAN: 0, MIN: 0, MAX: 0}; + +export const TEXTURE_WIDTH = 1024; + +/** + * This class manages the resources for performing the first step of aggregation + * Sort a list of data points into a number of bins + */ +export class WebGLBinSorter { + device: Device; + model: Model; + + /** + * A packed texture in which each pixel represents a bin. + * The index of the pixel in the memory layout is the bin index. + * Alpha value is the count of data points that fall into this bin + * R,G,B values are the aggregated weights of each channel: + * - Sum of all data points if operation is 'SUM' or 'MEAN' + * - Min of all data points if operation is 'MIN' + * - Max of all data points if operation is 'MAX' + */ + private binsFBO: Framebuffer | null = null; + + constructor(device: Device, settings: GPUAggregatorSettings) { + this.device = device; + this.model = createModel(device, settings); + } + + get texture(): Texture | null { + return this.binsFBO ? this.binsFBO.colorAttachments[0].texture : null; + } + + destroy() { + this.model.destroy(); + this.binsFBO?.colorAttachments[0].texture.destroy(); + this.binsFBO?.destroy(); + } + + getBinValues(index: number): Float32Array | null { + if (!this.binsFBO) { + return null; + } + const x = index % TEXTURE_WIDTH; + const y = Math.floor(index / TEXTURE_WIDTH); + const buffer = this.device.readPixelsToArrayWebGL(this.binsFBO, { + sourceX: x, + sourceY: y, + sourceWidth: 1, + sourceHeight: 1 + }).buffer; + return new Float32Array(buffer); + } + + setDimensions(numBins: number, stride: number = 0) { + const width = TEXTURE_WIDTH; + const height = Math.ceil(numBins / width); + + // Only destroy existing texture if it is not large enough + if (!this.binsFBO) { + this.binsFBO = createRenderTarget(this.device, width, height); + } else if (this.binsFBO.height < height) { + this.binsFBO.resize({width, height}); + } + + this.model.setUniforms({ + binStride: stride, + targetSize: [this.binsFBO.width, this.binsFBO.height] + }); + } + + setModelProps( + props: Pick< + ModelProps, + 'moduleSettings' | 'vertexCount' | 'uniforms' | 'attributes' | 'constantAttributes' + > + ) { + const model = this.model; + if (props.uniforms) { + model.setUniforms(props.uniforms); + } + if (props.attributes) { + model.setAttributes(props.attributes); + } + if (props.constantAttributes) { + model.setConstantAttributes(props.constantAttributes); + } + if (props.vertexCount !== undefined) { + model.setVertexCount(props.vertexCount); + } + if (props.moduleSettings) { + // TODO v9.1 - remove after migrate to UBO + model.updateModuleSettings(props.moduleSettings); + model.shaderInputs.setProps({project: props.moduleSettings}); + } + } + + /** Update aggregation */ + update( + /** The aggregation operation for each channel. Use null to skip update. */ + operations: (AggregationOperation | null)[] + ) { + const masks = getMaskByOperation(operations); + this._updateBins('SUM', masks.SUM + masks.MEAN); + this._updateBins('MIN', masks.MIN); + this._updateBins('MAX', masks.MAX); + } + + /** Recalculate aggregation on the given channels using the given operation */ + private _updateBins( + operation: AggregationOperation, + /** GPU bit mask of one or more channels that should be updated */ + colorMask: number + ) { + if (colorMask === 0) { + return; + } + colorMask |= COLOR_CHANNELS[3]; // Also renders to the alpha channel (point count) + + const model = this.model; + const target = this.binsFBO!; + + const initialValue = operation === 'MAX' ? -MAX_FLOAT32 : operation === 'MIN' ? MAX_FLOAT32 : 0; + const renderPass = this.device.beginRenderPass({ + id: `gpu-aggregation-${operation}`, + framebuffer: target, + parameters: { + viewport: [0, 0, target.width, target.height], + colorMask + }, + clearColor: [initialValue, initialValue, initialValue, 0], + clearDepth: false, + clearStencil: false + }); + model.setParameters({ + blendColorSrcFactor: 'one', + blendColorDstFactor: 'one', + blendAlphaSrcFactor: 'one', + blendAlphaDstFactor: 'one', + blendColorOperation: operation === 'MAX' ? 'max' : operation === 'MIN' ? 'min' : 'add', + blendAlphaOperation: 'add' + }); + model.draw(renderPass); + renderPass.end(); + } +} + +/** Convert a [channel -> operation] map to a [operation -> GPU color mask] map */ +function getMaskByOperation( + operations: (AggregationOperation | null)[] +): Record { + const result: Record = {...EMPTY_MASKS}; + for (let channel = 0; channel < operations.length; channel++) { + const op = operations[channel]; + if (op) { + result[op] += COLOR_CHANNELS[channel]; + } + } + return result; +} + +function createModel(device: Device, settings: GPUAggregatorSettings): Model { + let userVs = settings.vs; + + if (settings.dimensions === 2) { + // If user provides 2d bin IDs, convert them to 1d indices for data packing + userVs += glsl` +void getBin(out int binId) { + ivec2 binId2; + getBin(binId2); + if (binId2.x < 0 || binId2.x >= binStride) { + binId = -1; + } else { + binId = binId2.y * binStride + binId2.x; + } +} +`; + } + + const vs = `\ +#version 300 es +#define SHADER_NAME gpu-aggregation-sort-bins-vertex + +uniform int binStride; +uniform ivec2 targetSize; + +${userVs} + +out vec3 v_Weight; + +void main() { + int binIndex; + getBin(binIndex); + if (binIndex < 0) { + gl_Position = vec4(0.); + return; + } + int row = binIndex / targetSize.x; + int col = binIndex - row * targetSize.x; + vec2 position = (vec2(col, row) + 0.5) / vec2(targetSize) * 2.0 - 1.0; + gl_Position = vec4(position, 0.0, 1.0); + gl_PointSize = 1.0; + +#if NUM_CHANNELS == 3 + getWeight(v_Weight); +#elif NUM_CHANNELS == 2 + getWeight(v_Weight.xy); +#else + getWeight(v_Weight.x); +#endif +} +`; + const fs = glsl`\ +#version 300 es +#define SHADER_NAME gpu-aggregation-sort-bins-fragment + +precision highp float; + +in vec3 v_Weight; +out vec4 fragColor; + +void main() { + fragColor.xyz = v_Weight; + geometry.uv = vec2(0.); + DECKGL_FILTER_COLOR(fragColor, geometry); + fragColor.w = 1.0; +} +`; + const model = new Model(device, { + ...settings, + defines: {...settings.defines, NON_INSTANCED_MODEL: 1, NUM_CHANNELS: settings.numChannels}, + isInstanced: false, + vs, + fs, + topology: 'point-list' + }); + return model; +} From 2af6015de6a99d694a2c140942f75080f7b2a4f7 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sun, 12 May 2024 01:17:55 -0700 Subject: [PATCH 2/6] Unit tests --- .../gpu-aggregator/gpu-aggregator.ts | 50 ++-- .../webgl-aggregation-transform.ts | 28 +- .../gpu-aggregator/webgl-bin-sorter.ts | 23 +- .../aggregation-layer-v9/data-sample.ts | 34 +++ .../gpu-aggregator.spec.ts | 273 ++++++++++++++++++ .../aggregation-layer-v9/test-utils.ts | 34 +++ test/modules/aggregation-layers/index.ts | 2 + 7 files changed, 409 insertions(+), 35 deletions(-) create mode 100644 test/modules/aggregation-layers/aggregation-layer-v9/data-sample.ts create mode 100644 test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts create mode 100644 test/modules/aggregation-layers/aggregation-layer-v9/test-utils.ts diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts index 7849d80af51..d6e6af1c018 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts @@ -1,7 +1,7 @@ import {ModelProps} from '@luma.gl/engine'; import {WebGLBinSorter} from './webgl-bin-sorter'; import {WebGLAggregationTransform} from './webgl-aggregation-transform'; -import {_deepEqual as deepEqual, BinaryAttribute} from '@deck.gl/core'; +import {_deepEqual as deepEqual, log, BinaryAttribute} from '@deck.gl/core'; import type {Aggregator, AggregationProps} from '../aggregator'; import type {Device, Buffer, BufferLayout, TypedArray} from '@luma.gl/core'; @@ -16,8 +16,8 @@ export type GPUAggregatorSettings = { /** Buffer layout for input attributes */ bufferLayout?: BufferLayout[]; /** Define a shader function with one of the signatures - * `void getBin(out int binId)`: if binCount is a number - * `void getBin(out ivec2 binId)`: if binCount is a 2D array + * `void getBin(out int binId)`: if dimensions=1 + * `void getBin(out ivec2 binId)`: if dimensions=2 * And a shader function with one of the signatures * `void getWeight(out float weight)`: if numChannels=1 * `void getWeight(out vec2 weight)`: if numChannels=2 @@ -34,13 +34,9 @@ export type GPUAggregatorSettings = { /** Options used to run GPU aggregation, can be changed at any time */ export type GPUAggregationProps = AggregationProps & { - /** Number of bins, can be either 1D or 2D. - * - 1D: vertex shader should implement a getBin function that yields an int id. - * Only ids within the range of [0, binCount] are counted. - * - 2D: vertex shader should implement a getBin function that yields an ivec2 id. - * Only ids within the range of [[0, 0], binCount] are counted. + /** Limits of binId defined for each dimension. Ids outside of the [start, end) are ignored. */ - binCount: number | [number, number]; + binIdRange: [start: number, end: number][]; /** Context props passed to the shader modules */ moduleSettings?: ModelProps['moduleSettings']; }; @@ -62,7 +58,7 @@ export class GPUAggregator implements Aggregator { device: Device; props: GPUAggregationProps = { pointCount: 0, - binCount: 0, + binIdRange: [[0, 0]], operations: [], attributes: {}, binOptions: {} @@ -118,10 +114,16 @@ export class GPUAggregator implements Aggregator { if (index < 0 || index >= this.numBins) { return null; } - const {binCount} = this.props; - const id = Array.isArray(binCount) - ? ([index % binCount[0], Math.floor(index / binCount[0])] as [number, number]) - : index; + const {binIdRange} = this.props; + let id: number | [number, number]; + + if (this.dimensions === 1) { + id = index + binIdRange[0][0]; + } else { + const [[x0, x1], [y0]] = binIdRange; + const width = x1 - x0; + id = [(index % width) + x0, Math.floor(index / width) + y0]; + } const pixel = this.binSorter.getBinValues(index); if (!pixel) { @@ -151,12 +153,20 @@ export class GPUAggregator implements Aggregator { const oldProps = this.props; // Update local settings. These will set the flag this._needsUpdate - if ('binCount' in props && !deepEqual(props.binCount, oldProps.binCount, 1)) { - const binCount = props.binCount!; - this.numBins = Array.isArray(binCount) ? binCount[0] * binCount[1] : binCount; + if ('binIdRange' in props && !deepEqual(props.binIdRange, oldProps.binIdRange, 2)) { + const binIdRange = props.binIdRange!; + log.assert(binIdRange.length === this.dimensions); + + if (this.dimensions === 1) { + const [[x0, x1]] = binIdRange; + this.numBins = x1 - x0; + } else { + const [[x0, x1], [y0, y1]] = binIdRange; + this.numBins = (x1 - x0) * (y1 - y0); + } - this.binSorter.setDimensions(this.numBins, binCount[0]); - this.aggregationTransform.setDimensions(this.numBins, binCount[0]); + this.binSorter.setDimensions(this.numBins, binIdRange); + this.aggregationTransform.setDimensions(this.numBins, binIdRange); this.setNeedsUpdate(); } if (props.operations) { @@ -234,7 +244,7 @@ export class GPUAggregator implements Aggregator { // Render data to bins this.binSorter.update(operationsToUpdate); // Read to buffer and calculate domain - this.aggregationTransform.update(this.binSorter.texture!, operations); + this.aggregationTransform.update(this.binSorter.texture, operations); this.needsUpdate.fill(false); diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts index 5ebf0f0429a..5b821662443 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts @@ -55,15 +55,20 @@ export class WebGLAggregationTransform { return this._domains; } - setDimensions(numBins: number, stride: number = 0) { + setDimensions(numBins: number, binIdRange: [number, number][]) { const {model, transformFeedback} = this.transform; model.setVertexCount(numBins); model.setUniforms({ - binStride: stride + binIdRange: [ + binIdRange[0][0], + binIdRange[0][1], + binIdRange[1]?.[0] || 0, + binIdRange[1]?.[1] || 0 + ] }); // Only destroy existing buffer if it is not large enough - const binBufferByteLength = numBins * (stride ? 2 : 1) * 4; + const binBufferByteLength = numBins * binIdRange.length * 4; if (!this.binBuffer || this.binBuffer.byteLength < binBufferByteLength) { this.binBuffer?.destroy(); this.binBuffer = this.device.createBuffer({byteLength: binBufferByteLength}); @@ -78,7 +83,10 @@ export class WebGLAggregationTransform { } } - update(bins: Texture, operations: AggregationOperation[]) { + update(bins: Texture | null, operations: AggregationOperation[]) { + if (!bins) { + return; + } const transform = this.transform; const target = this.domainFBO; @@ -108,7 +116,7 @@ function createTransform(device: Device, settings: GPUAggregatorSettings): Buffe #version 300 es #define SHADER_NAME gpu-aggregation-domain-vertex -uniform int binStride; +uniform ivec4 binIdRange; uniform bvec3 isMean; uniform float naN; uniform sampler2D bins; @@ -137,12 +145,12 @@ void main() { } #if NUM_DIMS == 1 - binIds = float(gl_VertexID); + binIds = float(gl_VertexID + binIdRange.x); #else - int y = gl_VertexID / binStride; - int x = gl_VertexID - y * binStride; - binIds.y = float(y); - binIds.x = float(x); + int y = gl_VertexID / (binIdRange.y - binIdRange.x); + int x = gl_VertexID - y * (binIdRange.y - binIdRange.x); + binIds.y = float(y + binIdRange.z); + binIds.x = float(x + binIdRange.x); #endif #if NUM_CHANNELS == 3 diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts index 6ec800aa09c..223084b3102 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts @@ -60,7 +60,7 @@ export class WebGLBinSorter { return new Float32Array(buffer); } - setDimensions(numBins: number, stride: number = 0) { + setDimensions(numBins: number, binIdRange: [number, number][]) { const width = TEXTURE_WIDTH; const height = Math.ceil(numBins / width); @@ -72,7 +72,12 @@ export class WebGLBinSorter { } this.model.setUniforms({ - binStride: stride, + binIdRange: [ + binIdRange[0][0], + binIdRange[0][1], + binIdRange[1]?.[0] || 0, + binIdRange[1]?.[1] || 0 + ], targetSize: [this.binsFBO.width, this.binsFBO.height] }); } @@ -108,6 +113,9 @@ export class WebGLBinSorter { /** The aggregation operation for each channel. Use null to skip update. */ operations: (AggregationOperation | null)[] ) { + if (!this.binsFBO) { + return; + } const masks = getMaskByOperation(operations); this._updateBins('SUM', masks.SUM + masks.MEAN); this._updateBins('MIN', masks.MIN); @@ -176,10 +184,10 @@ function createModel(device: Device, settings: GPUAggregatorSettings): Model { void getBin(out int binId) { ivec2 binId2; getBin(binId2); - if (binId2.x < 0 || binId2.x >= binStride) { + if (binId2.x < binIdRange.x || binId2.x >= binIdRange.y) { binId = -1; } else { - binId = binId2.y * binStride + binId2.x; + binId = (binId2.y - binIdRange.z) * (binIdRange.y - binIdRange.x) + binId2.x; } } `; @@ -189,7 +197,7 @@ void getBin(out int binId) { #version 300 es #define SHADER_NAME gpu-aggregation-sort-bins-vertex -uniform int binStride; +uniform ivec4 binIdRange; uniform ivec2 targetSize; ${userVs} @@ -199,6 +207,7 @@ out vec3 v_Weight; void main() { int binIndex; getBin(binIndex); + binIndex = binIndex - binIdRange.x; if (binIndex < 0) { gl_Position = vec4(0.); return; @@ -229,8 +238,12 @@ out vec4 fragColor; void main() { fragColor.xyz = v_Weight; + + #ifdef MODULE_GEOMETRY geometry.uv = vec2(0.); DECKGL_FILTER_COLOR(fragColor, geometry); + #endif + fragColor.w = 1.0; } `; diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/data-sample.ts b/test/modules/aggregation-layers/aggregation-layer-v9/data-sample.ts new file mode 100644 index 00000000000..a02041e1337 --- /dev/null +++ b/test/modules/aggregation-layers/aggregation-layer-v9/data-sample.ts @@ -0,0 +1,34 @@ +export const IncomeSurvey = [ + {age: 19, household: 1, income: 25, education: 1}, + {age: 20, household: 1, income: 0, education: 2}, + {age: 21, household: 1, income: 0, education: 2}, + {age: 22, household: 2, income: 40, education: 2}, + {age: 23, household: 1, income: 80, education: 3}, + {age: 24, household: 1, income: 120, education: 3}, + {age: 25, household: 1, income: 0, education: 4}, + {age: 26, household: 2, income: 60, education: 2}, + {age: 27, household: 1, income: 75, education: 3}, + {age: 28, household: 3, income: 20, education: 4}, + {age: 29, household: 4, income: 115, education: 3}, + {age: 30, household: 2, income: 90, education: 3}, + {age: 32, household: 1, income: 150, education: 4}, + {age: 34, household: 2, income: 60, education: 5}, + {age: 36, household: 3, income: 90, education: 3}, + {age: 38, household: 5, income: 200, education: 4}, + {age: 40, household: 4, income: 140, education: 4}, + {age: 42, household: 2, income: 110, education: 5}, + {age: 44, household: 4, income: 500, education: 4}, + {age: 46, household: 3, income: 80, education: 3}, + {age: 48, household: 2, income: 65, education: 2}, + {age: 50, household: 4, income: 325, education: 3}, + {age: 53, household: 2, income: 180, education: 3}, + {age: 56, household: 1, income: 95, education: 3}, + {age: 59, household: 3, income: 120, education: 5}, + {age: 62, household: 2, income: 0, education: 3}, + {age: 65, household: 1, income: 225, education: 4}, + {age: 68, household: 2, income: 30, education: 2}, + {age: 72, household: 1, income: 0, education: 1}, + {age: 76, household: 5, income: 40, education: 2}, + {age: 80, household: 2, income: 20, education: 2}, + {age: 84, household: 1, income: 30, education: 3} +]; diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts new file mode 100644 index 00000000000..47f238be5a1 --- /dev/null +++ b/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts @@ -0,0 +1,273 @@ +import test from 'tape-promise/tape'; +import {Attribute} from '@deck.gl/core'; +import {GPUAggregator} from '@deck.gl/aggregation-layers/aggregation-layer-v9/gpu-aggregator/gpu-aggregator'; +import {device} from '@deck.gl/test-utils'; + +import {IncomeSurvey} from './data-sample'; +import {getResourceCounts, binaryAttributeToArray} from './test-utils'; + +test('GPUAggregator#resources', t => { + const oldResourceCounts = getResourceCounts(); + // An aggregator that calculates average income grouped by education + const aggregator = new GPUAggregator(device, { + dimensions: 1, + numChannels: 1, + bufferLayout: [ + {name: 'income', format: 'float32', stepMode: 'vertex'}, + {name: 'education', format: 'float32', stepMode: 'vertex'} + ], + vs: ` + in float income; + in float education; + + void getBin(out int binId) { + binId = int(education); + } + void getWeight(out float weight) { + weight = income; + } + ` + }); + + t.doesNotThrow(() => aggregator.update({}), 'Calling update() without setting props'); + t.notOk(aggregator.getResult(0)); + t.notOk(aggregator.getBin(0)); + + const attributes = { + income: new Attribute(device, {id: 'income', size: 1, type: 'float32', accessor: 'getIncome'}), + education: new Attribute(device, { + id: 'education', + size: 1, + type: 'float32', + accessor: 'getEducation' + }) + }; + attributes.income.setData({value: new Float32Array(IncomeSurvey.map(d => d.income))}); + attributes.education.setData({value: new Float32Array(IncomeSurvey.map(d => d.education))}); + + aggregator.setProps({ + pointCount: IncomeSurvey.length, + binIdRange: [[4, 9]], + attributes, + operations: ['MEAN'] + }); + + aggregator.update({}); + t.ok(aggregator.getResult(0)); + t.ok(aggregator.getBin(0)); + + // Resize buffers + aggregator.setProps({ + binIdRange: [[0, 15]] + }); + aggregator.update({}); + t.ok(aggregator.getResult(0)); + t.ok(aggregator.getBin(0)); + + attributes.income.delete(); + attributes.education.delete(); + aggregator.destroy(); + + t.deepEqual(getResourceCounts(), oldResourceCounts, 'GPU resources are deleted'); + + t.end(); +}); + +test('GPUAggregator#1D', t => { + // An aggregator that calculates: + // [0] total count [1] average income [2] highest education, grouped by age + const aggregator = new GPUAggregator(device, { + dimensions: 1, + numChannels: 3, + bufferLayout: [ + {name: 'age', format: 'float32', stepMode: 'vertex'}, + {name: 'income', format: 'float32', stepMode: 'vertex'}, + {name: 'education', format: 'float32', stepMode: 'vertex'} + ], + vs: ` + uniform float ageGroupSize; + in float age; + in float income; + in float education; + + void getBin(out int binId) { + binId = int(floor(age / ageGroupSize)); + } + void getWeight(out vec3 weight) { + weight = vec3(1.0, income, education); + } + ` + }); + + const attributes = { + age: new Attribute(device, {id: 'age', size: 1, type: 'float32', accessor: 'getAge'}), + income: new Attribute(device, {id: 'income', size: 1, type: 'float32', accessor: 'getIncome'}), + education: new Attribute(device, { + id: 'education', + size: 1, + type: 'float32', + accessor: 'getEducation' + }) + }; + attributes.age.setData({value: new Float32Array(IncomeSurvey.map(d => d.age))}); + attributes.income.setData({value: new Float32Array(IncomeSurvey.map(d => d.income))}); + attributes.education.setData({value: new Float32Array(IncomeSurvey.map(d => d.education))}); + + aggregator.setProps({ + pointCount: IncomeSurvey.length, + binIdRange: [[2, 17]], // age: 10..84 + attributes, + operations: ['SUM', 'MEAN', 'MAX'], + binOptions: {ageGroupSize: 5} + }); + + aggregator.update({}); + + t.is(aggregator.numBins, 15, 'numBins'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getBins(), aggregator.numBins), + // prettier-ignore + [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + 'getBins()' + ); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(0), aggregator.numBins), + // prettier-ignore + [NaN, 1, 5, 5, 3, 2, 3, 2, 2, 2, 1, 2, 1, 1, 2], + 'getResult() - total counts' + ); + t.deepEqual(aggregator.getResultDomain(0), [1, 5], 'getResultDomain() - counts'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(1), aggregator.numBins), + // prettier-ignore + [NaN, 25, 48, 54, 100, 145, 250, 72.5, 252.5, 107.5, 0, 127.5, 0, 40, 25], + 'getResult() - mean income' + ); + t.deepEqual(aggregator.getResultDomain(1), [0, 252.5], 'getResultDomain() - mean income'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(2), aggregator.numBins), + // prettier-ignore + [NaN, 1, 3, 4, 5, 4, 5, 3, 3, 5, 3, 4, 1, 2, 3], + 'getResult() - max education' + ); + t.deepEqual(aggregator.getResultDomain(2), [1, 5], 'getResultDomain() - max education'); + + // Empty bin + t.deepEqual(aggregator.getBin(0), {id: 2, count: 0, value: [NaN, NaN, NaN]}, 'getBin() - empty'); + // {age: 40, household: 4, income: 140, education: 4}, + // {age: 42, household: 2, income: 110, education: 5}, + // {age: 44, household: 4, income: 500, education: 4}, + t.deepEqual(aggregator.getBin(6), {id: 8, count: 3, value: [3, 250, 5]}, 'getBin()'); + + attributes.age.delete(); + attributes.income.delete(); + attributes.education.delete(); + aggregator.destroy(); + t.end(); +}); + +test('GPUAggregator#2D', t => { + // An aggregator that calculates: + // [0] total count [1] average income, grouped by [age, education] + const aggregator = new GPUAggregator(device, { + dimensions: 2, + numChannels: 2, + bufferLayout: [ + {name: 'age', format: 'float32', stepMode: 'vertex'}, + {name: 'income', format: 'float32', stepMode: 'vertex'}, + {name: 'education', format: 'float32', stepMode: 'vertex'} + ], + vs: ` + uniform float ageGroupSize; + in float age; + in float income; + in float education; + + void getBin(out ivec2 binId) { + binId.x = int(floor(age / ageGroupSize)); + binId.y = int(education); + } + void getWeight(out vec2 weight) { + weight = vec2(1.0, income); + } + ` + }); + + const attributes = { + age: new Attribute(device, {id: 'age', size: 1, type: 'float32', accessor: 'getAge'}), + income: new Attribute(device, {id: 'income', size: 1, type: 'float32', accessor: 'getIncome'}), + education: new Attribute(device, { + id: 'education', + size: 1, + type: 'float32', + accessor: 'getEducation' + }) + }; + attributes.age.setData({value: new Float32Array(IncomeSurvey.map(d => d.age))}); + attributes.income.setData({value: new Float32Array(IncomeSurvey.map(d => d.income))}); + attributes.education.setData({value: new Float32Array(IncomeSurvey.map(d => d.education))}); + + aggregator.setProps({ + pointCount: IncomeSurvey.length, + binIdRange: [ + [2, 6], + [1, 6] + ], // age: 20..59, education: 1..5 + attributes, + operations: ['SUM', 'MEAN'], + binOptions: {ageGroupSize: 10} + }); + + aggregator.update({}); + + t.is(aggregator.numBins, 20, 'numBins'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getBins(), aggregator.numBins), + // prettier-ignore + [ 2, 1, 3, 1, 4, 1, 5, 1, + 2, 2, 3, 2, 4, 2, 5, 2, + 2, 3, 3, 3, 4, 3, 5, 3, + 2, 4, 3, 4, 4, 4, 5, 4, + 2, 5, 3, 5, 4, 5, 5, 5 ], + 'getBins()' + ); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(0), aggregator.numBins), + // prettier-ignore + [ NaN, NaN, NaN, NaN, 4, + NaN, 1, NaN, 4, 2, + 1, 3, 2, 2, 2, + NaN, NaN, 1, 1, 1 ], + 'getResult() - total counts' + ); + t.deepEqual(aggregator.getResultDomain(0), [1, 4], 'getResultDomain() - counts'); + + t.deepEqual( + binaryAttributeToArray(aggregator.getResult(1), aggregator.numBins), + // prettier-ignore + [ NaN, NaN, NaN, NaN, 25, + NaN, 65, NaN, 97.5, 90, + 80, 200, 10, 175, 320, + NaN, NaN, 60, 110, 120 ], + 'getResult() - mean income' + ); + t.deepEqual(aggregator.getResultDomain(1), [10, 320], 'getResultDomain() - mean income'); + + // Empty bin + t.deepEqual(aggregator.getBin(0), {id: [2, 1], count: 0, value: [NaN, NaN]}, 'getBin() - empty'); + // {age: 40, household: 4, income: 140, education: 4}, + // {age: 44, household: 4, income: 500, education: 4}, + t.deepEqual(aggregator.getBin(14), {id: [4, 4], count: 2, value: [2, 320]}, 'getBin()'); + + attributes.age.delete(); + attributes.income.delete(); + attributes.education.delete(); + aggregator.destroy(); + t.end(); +}); diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/test-utils.ts b/test/modules/aggregation-layers/aggregation-layer-v9/test-utils.ts new file mode 100644 index 00000000000..d77e013993f --- /dev/null +++ b/test/modules/aggregation-layers/aggregation-layer-v9/test-utils.ts @@ -0,0 +1,34 @@ +import {BinaryAttribute} from '@deck.gl/core'; +import {luma} from '@luma.gl/core'; + +export function binaryAttributeToArray(accessor: BinaryAttribute, length: number): number[] { + let value = accessor.value; + if (!value) { + const buffer = accessor.buffer; + if (!buffer) { + throw new Error('Invalid binary attribute'); + } + value = new Float32Array(buffer.readSyncWebGL().buffer); + } + const size = accessor.size ?? 1; + const offset = (accessor.offset ?? 0) / 4; + const stride = accessor.stride ? accessor.stride / 4 : size!; + const result: number[] = []; + for (let i = 0; i < length; i++) { + for (let j = 0; j < size; j++) { + result[i * size + j] = value[i * stride + offset + j]; + } + } + return result; +} + +export function getResourceCounts(): Record { + /* global luma */ + const resourceStats = luma.stats.get('Resource Counts'); + return { + Framebuffer: resourceStats.get('Framebuffers Active').count, + Texture2D: resourceStats.get('Texture2Ds Active').count, + Buffer: resourceStats.get('Buffers Active').count, + Shader: resourceStats.get('Shaders Active').count + }; +} diff --git a/test/modules/aggregation-layers/index.ts b/test/modules/aggregation-layers/index.ts index 93dbb028298..2203da51cfa 100644 --- a/test/modules/aggregation-layers/index.ts +++ b/test/modules/aggregation-layers/index.ts @@ -40,3 +40,5 @@ import './utils/bin-sorter.spec'; import './utils/color-utils.spec'; // import './utils/gpu-grid-aggregator.spec'; import './utils/scale-utils.spec'; + +import './aggregation-layer-v9/gpu-aggregator.spec'; From 1b3cf9ece06ba741be708ab4b464ebd6ef36aadd Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sun, 12 May 2024 23:43:38 -0700 Subject: [PATCH 3/6] export GPUAggregator --- modules/aggregation-layers/src/index.ts | 7 +++++++ modules/main/src/index.ts | 3 ++- .../aggregation-layer-v9/gpu-aggregator.spec.ts | 2 +- .../aggregation-layer-v9/test-utils.ts | 9 ++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/aggregation-layers/src/index.ts b/modules/aggregation-layers/src/index.ts index a231fe12b7f..f15cfebe279 100644 --- a/modules/aggregation-layers/src/index.ts +++ b/modules/aggregation-layers/src/index.ts @@ -33,6 +33,8 @@ export {default as _CPUAggregator} from './utils/cpu-aggregator'; export {default as _AggregationLayer} from './aggregation-layer'; export {default as _BinSorter} from './utils/bin-sorter'; +export {GPUAggregator} from './aggregation-layer-v9/gpu-aggregator/gpu-aggregator'; + // types export type {ContourLayerProps} from './contour-layer/contour-layer'; export type {HeatmapLayerProps} from './heatmap-layer/heatmap-layer'; @@ -41,3 +43,8 @@ export type {CPUGridLayerProps} from './cpu-grid-layer/cpu-grid-layer'; export type {GridLayerProps} from './grid-layer/grid-layer'; export type {GPUGridLayerProps} from './gpu-grid-layer/gpu-grid-layer'; export type {ScreenGridLayerProps} from './screen-grid-layer/screen-grid-layer'; + +export type { + GPUAggregationProps, + GPUAggregatorSettings +} from './aggregation-layer-v9/gpu-aggregator/gpu-aggregator'; diff --git a/modules/main/src/index.ts b/modules/main/src/index.ts index d0b33509830..c5a1626a432 100644 --- a/modules/main/src/index.ts +++ b/modules/main/src/index.ts @@ -120,7 +120,8 @@ export { GridLayer, GPUGridLayer, AGGREGATION_OPERATION, - HeatmapLayer + HeatmapLayer, + GPUAggregator } from '@deck.gl/aggregation-layers'; export { diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts index 47f238be5a1..7549a7c48ff 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts @@ -1,6 +1,6 @@ import test from 'tape-promise/tape'; import {Attribute} from '@deck.gl/core'; -import {GPUAggregator} from '@deck.gl/aggregation-layers/aggregation-layer-v9/gpu-aggregator/gpu-aggregator'; +import {GPUAggregator} from '@deck.gl/aggregation-layers'; import {device} from '@deck.gl/test-utils'; import {IncomeSurvey} from './data-sample'; diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/test-utils.ts b/test/modules/aggregation-layers/aggregation-layer-v9/test-utils.ts index d77e013993f..584087dc1ff 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/test-utils.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/test-utils.ts @@ -1,7 +1,14 @@ import {BinaryAttribute} from '@deck.gl/core'; import {luma} from '@luma.gl/core'; -export function binaryAttributeToArray(accessor: BinaryAttribute, length: number): number[] { +export function binaryAttributeToArray( + accessor: BinaryAttribute | null, + length: number +): number[] | null { + if (!accessor) { + return null; + } + let value = accessor.value; if (!value) { const buffer = accessor.buffer; From 2f790c7f94b0ad25421ec5359d706c8e3fb978b8 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sun, 9 Jun 2024 16:46:03 -0700 Subject: [PATCH 4/6] interface updates --- .../gpu-aggregator/gpu-aggregator.ts | 20 +++++++------ .../webgl-aggregation-transform.ts | 16 ++++++++--- .../gpu-aggregator/webgl-bin-sorter.ts | 18 ++++++------ .../gpu-aggregator.spec.ts | 28 ++++++++++--------- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts index d6e6af1c018..e2efd903494 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts @@ -19,9 +19,9 @@ export type GPUAggregatorSettings = { * `void getBin(out int binId)`: if dimensions=1 * `void getBin(out ivec2 binId)`: if dimensions=2 * And a shader function with one of the signatures - * `void getWeight(out float weight)`: if numChannels=1 - * `void getWeight(out vec2 weight)`: if numChannels=2 - * `void getWeight(out vec3 weight)`: if numChannels=3 + * `void getValue(out float value)`: if numChannels=1 + * `void getValue(out vec2 value)`: if numChannels=2 + * `void getValue(out vec3 value)`: if numChannels=3 */ vs: string; /** Shader modules @@ -132,11 +132,13 @@ export class GPUAggregator implements Aggregator { const count = pixel[3]; const value: number[] = []; for (let channel = 0; channel < this.numChannels; channel++) { - if (count === 0) { + const operation = this.props.operations[channel]; + if (operation === 'COUNT') { + value[channel] = count; + } else if (count === 0) { value[channel] = NaN; } else { - value[channel] = - this.props.operations[channel] === 'MEAN' ? pixel[channel] / count : pixel[channel]; + value[channel] = operation === 'MEAN' ? pixel[channel] / count : pixel[channel]; } } return {id, value, count}; @@ -222,11 +224,13 @@ export class GPUAggregator implements Aggregator { } } + update() {} + /** Run aggregation */ - update( + preDraw( /** Parameters only available at runtime */ opts: { - moduleSettings?: ModelProps['moduleSettings']; + moduleSettings: any; } ) { if (!this.needsUpdate.some(Boolean)) { diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts index 5b821662443..d429fdbac19 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts @@ -91,6 +91,7 @@ export class WebGLAggregationTransform { const target = this.domainFBO; transform.model.setUniforms({ + isCount: Array.from({length: 3}, (_, i) => (operations[i] === 'COUNT' ? 1 : 0)), isMean: Array.from({length: 3}, (_, i) => (operations[i] === 'MEAN' ? 1 : 0)) }); transform.model.setBindings({bins}); @@ -117,6 +118,7 @@ function createTransform(device: Device, settings: GPUAggregatorSettings): Buffe #define SHADER_NAME gpu-aggregation-domain-vertex uniform ivec4 binIdRange; +uniform bvec3 isCount; uniform bvec3 isMean; uniform float naN; uniform sampler2D bins; @@ -136,10 +138,14 @@ out vec3 values; #endif void main() { - int row = gl_VertexID / SAMEPLER_WIDTH; - int col = gl_VertexID - row * SAMEPLER_WIDTH; + int row = gl_VertexID / SAMPLER_WIDTH; + int col = gl_VertexID - row * SAMPLER_WIDTH; vec4 weights = texelFetch(bins, ivec2(col, row), 0); - vec3 value3 = mix(weights.rgb, weights.rgb / max(weights.a, 1.0), isMean); + vec3 value3 = mix( + mix(weights.rgb, vec3(weights.a), isCount), + weights.rgb / max(weights.a, 1.0), + isMean + ); if (weights.a == 0.0) { value3 = vec3(naN); } @@ -162,6 +168,8 @@ void main() { #endif gl_Position = vec4(0., 0., 0., 1.); + // This model renders into a 2x1 texture to obtain min and max simultaneously. + // See comments in fragment shader gl_PointSize = 2.0; } `; @@ -222,7 +230,7 @@ void main() { defines: { NUM_DIMS: settings.dimensions, NUM_CHANNELS: settings.numChannels, - SAMEPLER_WIDTH: TEXTURE_WIDTH + SAMPLER_WIDTH: TEXTURE_WIDTH }, uniforms: { // Passed in as uniform because 1) there is no GLSL symbol for NaN 2) any expression that exploits undefined behavior to produces NaN diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts index 223084b3102..cb486aded14 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts @@ -7,7 +7,7 @@ import type {AggregationOperation} from '../aggregator'; const COLOR_CHANNELS = [0x1, 0x2, 0x4, 0x8]; // GPU color mask RED, GREEN, BLUE, ALPHA const MAX_FLOAT32 = 3e38; -const EMPTY_MASKS = {SUM: 0, MEAN: 0, MIN: 0, MAX: 0}; +const EMPTY_MASKS = {SUM: 0, MEAN: 0, MIN: 0, MAX: 0, COUNT: 0}; export const TEXTURE_WIDTH = 1024; @@ -23,8 +23,8 @@ export class WebGLBinSorter { * A packed texture in which each pixel represents a bin. * The index of the pixel in the memory layout is the bin index. * Alpha value is the count of data points that fall into this bin - * R,G,B values are the aggregated weights of each channel: - * - Sum of all data points if operation is 'SUM' or 'MEAN' + * R,G,B values are the aggregated values of each channel: + * - Sum of all data points if operation is 'SUM', or 'MEAN' * - Min of all data points if operation is 'MIN' * - Max of all data points if operation is 'MAX' */ @@ -202,7 +202,7 @@ uniform ivec2 targetSize; ${userVs} -out vec3 v_Weight; +out vec3 v_Value; void main() { int binIndex; @@ -219,11 +219,11 @@ void main() { gl_PointSize = 1.0; #if NUM_CHANNELS == 3 - getWeight(v_Weight); + getValue(v_Value); #elif NUM_CHANNELS == 2 - getWeight(v_Weight.xy); + getValue(v_Value.xy); #else - getWeight(v_Weight.x); + getValue(v_Value.x); #endif } `; @@ -233,11 +233,11 @@ void main() { precision highp float; -in vec3 v_Weight; +in vec3 v_Value; out vec4 fragColor; void main() { - fragColor.xyz = v_Weight; + fragColor.xyz = v_Value; #ifdef MODULE_GEOMETRY geometry.uv = vec2(0.); diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts index 7549a7c48ff..0df359e3704 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts @@ -23,13 +23,13 @@ test('GPUAggregator#resources', t => { void getBin(out int binId) { binId = int(education); } - void getWeight(out float weight) { - weight = income; + void getValue(out float value) { + value = income; } ` }); - t.doesNotThrow(() => aggregator.update({}), 'Calling update() without setting props'); + t.doesNotThrow(() => aggregator.update(), 'Calling update() without setting props'); t.notOk(aggregator.getResult(0)); t.notOk(aggregator.getBin(0)); @@ -52,7 +52,7 @@ test('GPUAggregator#resources', t => { operations: ['MEAN'] }); - aggregator.update({}); + aggregator.update(); t.ok(aggregator.getResult(0)); t.ok(aggregator.getBin(0)); @@ -60,7 +60,7 @@ test('GPUAggregator#resources', t => { aggregator.setProps({ binIdRange: [[0, 15]] }); - aggregator.update({}); + aggregator.update(); t.ok(aggregator.getResult(0)); t.ok(aggregator.getBin(0)); @@ -93,8 +93,8 @@ test('GPUAggregator#1D', t => { void getBin(out int binId) { binId = int(floor(age / ageGroupSize)); } - void getWeight(out vec3 weight) { - weight = vec3(1.0, income, education); + void getValue(out vec3 value) { + value = vec3(1.0, income, education); } ` }); @@ -121,7 +121,8 @@ test('GPUAggregator#1D', t => { binOptions: {ageGroupSize: 5} }); - aggregator.update({}); + aggregator.update(); + aggregator.preDraw({moduleSettings: {}}); t.is(aggregator.numBins, 15, 'numBins'); @@ -191,8 +192,8 @@ test('GPUAggregator#2D', t => { binId.x = int(floor(age / ageGroupSize)); binId.y = int(education); } - void getWeight(out vec2 weight) { - weight = vec2(1.0, income); + void getValue(out vec2 value) { + value = vec2(5.0, income); } ` }); @@ -218,11 +219,12 @@ test('GPUAggregator#2D', t => { [1, 6] ], // age: 20..59, education: 1..5 attributes, - operations: ['SUM', 'MEAN'], + operations: ['COUNT', 'MEAN'], binOptions: {ageGroupSize: 10} }); - aggregator.update({}); + aggregator.update(); + aggregator.preDraw({moduleSettings: {}}); t.is(aggregator.numBins, 20, 'numBins'); @@ -260,7 +262,7 @@ test('GPUAggregator#2D', t => { t.deepEqual(aggregator.getResultDomain(1), [10, 320], 'getResultDomain() - mean income'); // Empty bin - t.deepEqual(aggregator.getBin(0), {id: [2, 1], count: 0, value: [NaN, NaN]}, 'getBin() - empty'); + t.deepEqual(aggregator.getBin(0), {id: [2, 1], count: 0, value: [0, NaN]}, 'getBin() - empty'); // {age: 40, household: 4, income: 140, education: 4}, // {age: 44, household: 4, income: 500, education: 4}, t.deepEqual(aggregator.getBin(14), {id: [4, 4], count: 2, value: [2, 320]}, 'getBin()'); From a9f05040c1130e07486b1bab5461e34bc93cc1e4 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Sun, 9 Jun 2024 17:45:47 -0700 Subject: [PATCH 5/6] type improvements --- .../gpu-aggregator/gpu-aggregator.ts | 23 +++++++------------ .../webgl-aggregation-transform.ts | 6 ++--- .../gpu-aggregator/webgl-bin-sorter.ts | 6 ++--- modules/aggregation-layers/src/index.ts | 2 +- .../gpu-aggregator.spec.ts | 8 +++++-- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts index e2efd903494..f5f546a3d8b 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts @@ -3,12 +3,12 @@ import {WebGLBinSorter} from './webgl-bin-sorter'; import {WebGLAggregationTransform} from './webgl-aggregation-transform'; import {_deepEqual as deepEqual, log, BinaryAttribute} from '@deck.gl/core'; -import type {Aggregator, AggregationProps} from '../aggregator'; +import type {Aggregator, AggregationProps, AggregatedBin} from '../aggregator'; import type {Device, Buffer, BufferLayout, TypedArray} from '@luma.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; -/** Settings used to construct a new GPUAggregator */ -export type GPUAggregatorSettings = { +/** Options used to construct a new GPUAggregator */ +export type GPUAggregatorOptions = { /** Size of bin IDs */ dimensions: 1 | 2; /** How many properties to perform aggregation on */ @@ -32,7 +32,7 @@ export type GPUAggregatorSettings = { defines?: Record; }; -/** Options used to run GPU aggregation, can be changed at any time */ +/** Props used to run GPU aggregation, can be changed at any time */ export type GPUAggregationProps = AggregationProps & { /** Limits of binId defined for each dimension. Ids outside of the [start, end) are ignored. */ @@ -71,7 +71,7 @@ export class GPUAggregator implements Aggregator { /** Step 2. (optional) calculate the min/max across all bins */ protected aggregationTransform: WebGLAggregationTransform; - constructor(device: Device, settings: GPUAggregatorSettings) { + constructor(device: Device, settings: GPUAggregatorOptions) { this.device = device; this.dimensions = settings.dimensions; this.numChannels = settings.numChannels; @@ -103,22 +103,15 @@ export class GPUAggregator implements Aggregator { } /** Returns the information for a given bin. */ - getBin(index: number): { - /** The original id */ - id: number | [number, number]; - /** Aggregated values by channel */ - value: number[]; - /** Count of data points in this bin */ - count: number; - } | null { + getBin(index: number): AggregatedBin | null { if (index < 0 || index >= this.numBins) { return null; } const {binIdRange} = this.props; - let id: number | [number, number]; + let id: number[]; if (this.dimensions === 1) { - id = index + binIdRange[0][0]; + id = [index + binIdRange[0][0]]; } else { const [[x0, x1], [y0]] = binIdRange; const width = x1 - x0; diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts index d429fdbac19..26308ff8c8c 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts @@ -2,7 +2,7 @@ import {BufferTransform} from '@luma.gl/engine'; import {glsl, createRenderTarget} from './utils'; import type {Device, Framebuffer, Buffer, Texture} from '@luma.gl/core'; -import type {GPUAggregatorSettings} from './gpu-aggregator'; +import type {GPUAggregatorOptions} from './gpu-aggregator'; import type {AggregationOperation} from '../aggregator'; import {TEXTURE_WIDTH} from './webgl-bin-sorter'; @@ -26,7 +26,7 @@ export class WebGLAggregationTransform { /** Aggregated [min, max] for each channel */ private _domains: [min: number, max: number][] | null = null; - constructor(device: Device, settings: GPUAggregatorSettings) { + constructor(device: Device, settings: GPUAggregatorOptions) { this.device = device; this.numChannels = settings.numChannels; this.transform = createTransform(device, settings); @@ -112,7 +112,7 @@ export class WebGLAggregationTransform { } } -function createTransform(device: Device, settings: GPUAggregatorSettings): BufferTransform { +function createTransform(device: Device, settings: GPUAggregatorOptions): BufferTransform { const vs = glsl`\ #version 300 es #define SHADER_NAME gpu-aggregation-domain-vertex diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts index cb486aded14..173f0f2795c 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts @@ -2,7 +2,7 @@ import {Model, ModelProps} from '@luma.gl/engine'; import {glsl, createRenderTarget} from './utils'; import type {Device, Framebuffer, Texture} from '@luma.gl/core'; -import type {GPUAggregatorSettings} from './gpu-aggregator'; +import type {GPUAggregatorOptions} from './gpu-aggregator'; import type {AggregationOperation} from '../aggregator'; const COLOR_CHANNELS = [0x1, 0x2, 0x4, 0x8]; // GPU color mask RED, GREEN, BLUE, ALPHA @@ -30,7 +30,7 @@ export class WebGLBinSorter { */ private binsFBO: Framebuffer | null = null; - constructor(device: Device, settings: GPUAggregatorSettings) { + constructor(device: Device, settings: GPUAggregatorOptions) { this.device = device; this.model = createModel(device, settings); } @@ -175,7 +175,7 @@ function getMaskByOperation( return result; } -function createModel(device: Device, settings: GPUAggregatorSettings): Model { +function createModel(device: Device, settings: GPUAggregatorOptions): Model { let userVs = settings.vs; if (settings.dimensions === 2) { diff --git a/modules/aggregation-layers/src/index.ts b/modules/aggregation-layers/src/index.ts index f15cfebe279..a2f115ef8b6 100644 --- a/modules/aggregation-layers/src/index.ts +++ b/modules/aggregation-layers/src/index.ts @@ -46,5 +46,5 @@ export type {ScreenGridLayerProps} from './screen-grid-layer/screen-grid-layer'; export type { GPUAggregationProps, - GPUAggregatorSettings + GPUAggregatorOptions } from './aggregation-layer-v9/gpu-aggregator/gpu-aggregator'; diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts index 0df359e3704..c9f924e4cf0 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts @@ -158,11 +158,15 @@ test('GPUAggregator#1D', t => { t.deepEqual(aggregator.getResultDomain(2), [1, 5], 'getResultDomain() - max education'); // Empty bin - t.deepEqual(aggregator.getBin(0), {id: 2, count: 0, value: [NaN, NaN, NaN]}, 'getBin() - empty'); + t.deepEqual( + aggregator.getBin(0), + {id: [2], count: 0, value: [NaN, NaN, NaN]}, + 'getBin() - empty' + ); // {age: 40, household: 4, income: 140, education: 4}, // {age: 42, household: 2, income: 110, education: 5}, // {age: 44, household: 4, income: 500, education: 4}, - t.deepEqual(aggregator.getBin(6), {id: 8, count: 3, value: [3, 250, 5]}, 'getBin()'); + t.deepEqual(aggregator.getBin(6), {id: [8], count: 3, value: [3, 250, 5]}, 'getBin()'); attributes.age.delete(); attributes.income.delete(); From aa8b3fd84d1928101e3a0d74a336d8a409215116 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 11 Jun 2024 07:28:50 -0700 Subject: [PATCH 6/6] rename --- .../src/aggregation-layer-v9/aggregator.ts | 2 +- .../webgl-aggregation-transform.ts | 22 ++++----- ...{gpu-aggregator.ts => webgl-aggregator.ts} | 48 +++++++++---------- .../gpu-aggregator/webgl-bin-sorter.ts | 12 ++--- modules/aggregation-layers/src/index.ts | 8 ++-- modules/main/src/index.ts | 2 +- ...gator.spec.ts => webgl-aggregator.spec.ts} | 38 +++++++-------- test/modules/aggregation-layers/index.ts | 2 +- 8 files changed, 67 insertions(+), 67 deletions(-) rename modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/{gpu-aggregator.ts => webgl-aggregator.ts} (85%) rename test/modules/aggregation-layers/aggregation-layer-v9/{gpu-aggregator.spec.ts => webgl-aggregator.spec.ts} (88%) diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/aggregator.ts index c76ddc9e65c..5f432d8dc89 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/aggregator.ts @@ -63,7 +63,7 @@ export interface Aggregator { destroy(): void; /** Get the number of bins */ - get numBins(): number; + get binCount(): number; /** Returns an accessor to the bins. */ getBins(): BinaryAttribute | null; diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts index 26308ff8c8c..742a18737be 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregation-transform.ts @@ -2,7 +2,7 @@ import {BufferTransform} from '@luma.gl/engine'; import {glsl, createRenderTarget} from './utils'; import type {Device, Framebuffer, Buffer, Texture} from '@luma.gl/core'; -import type {GPUAggregatorOptions} from './gpu-aggregator'; +import type {WebGLAggregatorOptions} from './webgl-aggregator'; import type {AggregationOperation} from '../aggregator'; import {TEXTURE_WIDTH} from './webgl-bin-sorter'; @@ -11,7 +11,7 @@ const MAX_FLOAT32 = 3e38; export class WebGLAggregationTransform { device: Device; - numChannels: number; + channelCount: number; /** Packed from bin ids */ binBuffer: Buffer | null = null; @@ -26,9 +26,9 @@ export class WebGLAggregationTransform { /** Aggregated [min, max] for each channel */ private _domains: [min: number, max: number][] | null = null; - constructor(device: Device, settings: GPUAggregatorOptions) { + constructor(device: Device, settings: WebGLAggregatorOptions) { this.device = device; - this.numChannels = settings.numChannels; + this.channelCount = settings.channelCount; this.transform = createTransform(device, settings); this.domainFBO = createRenderTarget(device, 2, 1); } @@ -50,14 +50,14 @@ export class WebGLAggregationTransform { [-domain[4], domain[0]], [-domain[5], domain[1]], [-domain[6], domain[2]] - ].slice(0, this.numChannels) as [number, number][]; + ].slice(0, this.channelCount) as [number, number][]; } return this._domains; } - setDimensions(numBins: number, binIdRange: [number, number][]) { + setDimensions(binCount: number, binIdRange: [number, number][]) { const {model, transformFeedback} = this.transform; - model.setVertexCount(numBins); + model.setVertexCount(binCount); model.setUniforms({ binIdRange: [ binIdRange[0][0], @@ -68,14 +68,14 @@ export class WebGLAggregationTransform { }); // Only destroy existing buffer if it is not large enough - const binBufferByteLength = numBins * binIdRange.length * 4; + const binBufferByteLength = binCount * binIdRange.length * 4; if (!this.binBuffer || this.binBuffer.byteLength < binBufferByteLength) { this.binBuffer?.destroy(); this.binBuffer = this.device.createBuffer({byteLength: binBufferByteLength}); transformFeedback.setBuffer('binIds', this.binBuffer); } - const valueBufferByteLength = numBins * this.numChannels * 4; + const valueBufferByteLength = binCount * this.channelCount * 4; if (!this.valueBuffer || this.valueBuffer.byteLength < valueBufferByteLength) { this.valueBuffer?.destroy(); this.valueBuffer = this.device.createBuffer({byteLength: valueBufferByteLength}); @@ -112,7 +112,7 @@ export class WebGLAggregationTransform { } } -function createTransform(device: Device, settings: GPUAggregatorOptions): BufferTransform { +function createTransform(device: Device, settings: WebGLAggregatorOptions): BufferTransform { const vs = glsl`\ #version 300 es #define SHADER_NAME gpu-aggregation-domain-vertex @@ -229,7 +229,7 @@ void main() { }, defines: { NUM_DIMS: settings.dimensions, - NUM_CHANNELS: settings.numChannels, + NUM_CHANNELS: settings.channelCount, SAMPLER_WIDTH: TEXTURE_WIDTH }, uniforms: { diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregator.ts similarity index 85% rename from modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts rename to modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregator.ts index f5f546a3d8b..a16ff4fe694 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/gpu-aggregator.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-aggregator.ts @@ -7,21 +7,21 @@ import type {Aggregator, AggregationProps, AggregatedBin} from '../aggregator'; import type {Device, Buffer, BufferLayout, TypedArray} from '@luma.gl/core'; import type {ShaderModule} from '@luma.gl/shadertools'; -/** Options used to construct a new GPUAggregator */ -export type GPUAggregatorOptions = { +/** Options used to construct a new WebGLAggregator */ +export type WebGLAggregatorOptions = { /** Size of bin IDs */ dimensions: 1 | 2; /** How many properties to perform aggregation on */ - numChannels: 1 | 2 | 3; + channelCount: 1 | 2 | 3; /** Buffer layout for input attributes */ bufferLayout?: BufferLayout[]; /** Define a shader function with one of the signatures * `void getBin(out int binId)`: if dimensions=1 * `void getBin(out ivec2 binId)`: if dimensions=2 * And a shader function with one of the signatures - * `void getValue(out float value)`: if numChannels=1 - * `void getValue(out vec2 value)`: if numChannels=2 - * `void getValue(out vec3 value)`: if numChannels=3 + * `void getValue(out float value)`: if channelCount=1 + * `void getValue(out vec2 value)`: if channelCount=2 + * `void getValue(out vec3 value)`: if channelCount=3 */ vs: string; /** Shader modules @@ -33,7 +33,7 @@ export type GPUAggregatorOptions = { }; /** Props used to run GPU aggregation, can be changed at any time */ -export type GPUAggregationProps = AggregationProps & { +export type WebGLAggregationProps = AggregationProps & { /** Limits of binId defined for each dimension. Ids outside of the [start, end) are ignored. */ binIdRange: [start: number, end: number][]; @@ -42,7 +42,7 @@ export type GPUAggregationProps = AggregationProps & { }; /** An Aggregator implementation that calculates aggregation on the GPU */ -export class GPUAggregator implements Aggregator { +export class WebGLAggregator implements Aggregator { /** Checks if the current device supports GPU aggregation */ static isSupported(device: Device): boolean { return ( @@ -52,11 +52,11 @@ export class GPUAggregator implements Aggregator { } dimensions: 1 | 2; - numChannels: 1 | 2 | 3; - numBins: number = 0; + channelCount: 1 | 2 | 3; + binCount: number = 0; device: Device; - props: GPUAggregationProps = { + props: WebGLAggregationProps = { pointCount: 0, binIdRange: [[0, 0]], operations: [], @@ -71,11 +71,11 @@ export class GPUAggregator implements Aggregator { /** Step 2. (optional) calculate the min/max across all bins */ protected aggregationTransform: WebGLAggregationTransform; - constructor(device: Device, settings: GPUAggregatorOptions) { + constructor(device: Device, settings: WebGLAggregatorOptions) { this.device = device; this.dimensions = settings.dimensions; - this.numChannels = settings.numChannels; - this.needsUpdate = new Array(this.numChannels).fill(true); + this.channelCount = settings.channelCount; + this.needsUpdate = new Array(this.channelCount).fill(true); this.binSorter = new WebGLBinSorter(device, settings); this.aggregationTransform = new WebGLAggregationTransform(device, settings); } @@ -91,10 +91,10 @@ export class GPUAggregator implements Aggregator { /** Returns an accessor to the output for a given channel. */ getResult(channel: 0 | 1 | 2): BinaryAttribute | null { const buffer = this.aggregationTransform.valueBuffer; - if (!buffer || channel >= this.numChannels) { + if (!buffer || channel >= this.channelCount) { return null; } - return {buffer, type: 'float32', size: 1, stride: this.numChannels * 4, offset: channel * 4}; + return {buffer, type: 'float32', size: 1, stride: this.channelCount * 4, offset: channel * 4}; } /** Returns the [min, max] of aggregated values for a given channel. */ @@ -104,7 +104,7 @@ export class GPUAggregator implements Aggregator { /** Returns the information for a given bin. */ getBin(index: number): AggregatedBin | null { - if (index < 0 || index >= this.numBins) { + if (index < 0 || index >= this.binCount) { return null; } const {binIdRange} = this.props; @@ -124,7 +124,7 @@ export class GPUAggregator implements Aggregator { } const count = pixel[3]; const value: number[] = []; - for (let channel = 0; channel < this.numChannels; channel++) { + for (let channel = 0; channel < this.channelCount; channel++) { const operation = this.props.operations[channel]; if (operation === 'COUNT') { value[channel] = count; @@ -144,7 +144,7 @@ export class GPUAggregator implements Aggregator { } /** Update aggregation props. Normalize prop values and set change flags. */ - setProps(props: Partial) { + setProps(props: Partial) { const oldProps = this.props; // Update local settings. These will set the flag this._needsUpdate @@ -154,18 +154,18 @@ export class GPUAggregator implements Aggregator { if (this.dimensions === 1) { const [[x0, x1]] = binIdRange; - this.numBins = x1 - x0; + this.binCount = x1 - x0; } else { const [[x0, x1], [y0, y1]] = binIdRange; - this.numBins = (x1 - x0) * (y1 - y0); + this.binCount = (x1 - x0) * (y1 - y0); } - this.binSorter.setDimensions(this.numBins, binIdRange); - this.aggregationTransform.setDimensions(this.numBins, binIdRange); + this.binSorter.setDimensions(this.binCount, binIdRange); + this.aggregationTransform.setDimensions(this.binCount, binIdRange); this.setNeedsUpdate(); } if (props.operations) { - for (let channel = 0; channel < this.numChannels; channel++) { + for (let channel = 0; channel < this.channelCount; channel++) { if (props.operations[channel] !== oldProps.operations[channel]) { this.setNeedsUpdate(channel); } diff --git a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts index 173f0f2795c..e42c6113dc2 100644 --- a/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts +++ b/modules/aggregation-layers/src/aggregation-layer-v9/gpu-aggregator/webgl-bin-sorter.ts @@ -2,7 +2,7 @@ import {Model, ModelProps} from '@luma.gl/engine'; import {glsl, createRenderTarget} from './utils'; import type {Device, Framebuffer, Texture} from '@luma.gl/core'; -import type {GPUAggregatorOptions} from './gpu-aggregator'; +import type {WebGLAggregatorOptions} from './webgl-aggregator'; import type {AggregationOperation} from '../aggregator'; const COLOR_CHANNELS = [0x1, 0x2, 0x4, 0x8]; // GPU color mask RED, GREEN, BLUE, ALPHA @@ -30,7 +30,7 @@ export class WebGLBinSorter { */ private binsFBO: Framebuffer | null = null; - constructor(device: Device, settings: GPUAggregatorOptions) { + constructor(device: Device, settings: WebGLAggregatorOptions) { this.device = device; this.model = createModel(device, settings); } @@ -60,9 +60,9 @@ export class WebGLBinSorter { return new Float32Array(buffer); } - setDimensions(numBins: number, binIdRange: [number, number][]) { + setDimensions(binCount: number, binIdRange: [number, number][]) { const width = TEXTURE_WIDTH; - const height = Math.ceil(numBins / width); + const height = Math.ceil(binCount / width); // Only destroy existing texture if it is not large enough if (!this.binsFBO) { @@ -175,7 +175,7 @@ function getMaskByOperation( return result; } -function createModel(device: Device, settings: GPUAggregatorOptions): Model { +function createModel(device: Device, settings: WebGLAggregatorOptions): Model { let userVs = settings.vs; if (settings.dimensions === 2) { @@ -249,7 +249,7 @@ void main() { `; const model = new Model(device, { ...settings, - defines: {...settings.defines, NON_INSTANCED_MODEL: 1, NUM_CHANNELS: settings.numChannels}, + defines: {...settings.defines, NON_INSTANCED_MODEL: 1, NUM_CHANNELS: settings.channelCount}, isInstanced: false, vs, fs, diff --git a/modules/aggregation-layers/src/index.ts b/modules/aggregation-layers/src/index.ts index a2f115ef8b6..8cbd48ee334 100644 --- a/modules/aggregation-layers/src/index.ts +++ b/modules/aggregation-layers/src/index.ts @@ -33,7 +33,7 @@ export {default as _CPUAggregator} from './utils/cpu-aggregator'; export {default as _AggregationLayer} from './aggregation-layer'; export {default as _BinSorter} from './utils/bin-sorter'; -export {GPUAggregator} from './aggregation-layer-v9/gpu-aggregator/gpu-aggregator'; +export {WebGLAggregator} from './aggregation-layer-v9/gpu-aggregator/webgl-aggregator'; // types export type {ContourLayerProps} from './contour-layer/contour-layer'; @@ -45,6 +45,6 @@ export type {GPUGridLayerProps} from './gpu-grid-layer/gpu-grid-layer'; export type {ScreenGridLayerProps} from './screen-grid-layer/screen-grid-layer'; export type { - GPUAggregationProps, - GPUAggregatorOptions -} from './aggregation-layer-v9/gpu-aggregator/gpu-aggregator'; + WebGLAggregationProps, + WebGLAggregatorOptions +} from './aggregation-layer-v9/gpu-aggregator/webgl-aggregator'; diff --git a/modules/main/src/index.ts b/modules/main/src/index.ts index c5a1626a432..71d0af760e0 100644 --- a/modules/main/src/index.ts +++ b/modules/main/src/index.ts @@ -121,7 +121,7 @@ export { GPUGridLayer, AGGREGATION_OPERATION, HeatmapLayer, - GPUAggregator + WebGLAggregator } from '@deck.gl/aggregation-layers'; export { diff --git a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts b/test/modules/aggregation-layers/aggregation-layer-v9/webgl-aggregator.spec.ts similarity index 88% rename from test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts rename to test/modules/aggregation-layers/aggregation-layer-v9/webgl-aggregator.spec.ts index c9f924e4cf0..ddd6af5e422 100644 --- a/test/modules/aggregation-layers/aggregation-layer-v9/gpu-aggregator.spec.ts +++ b/test/modules/aggregation-layers/aggregation-layer-v9/webgl-aggregator.spec.ts @@ -1,17 +1,17 @@ import test from 'tape-promise/tape'; import {Attribute} from '@deck.gl/core'; -import {GPUAggregator} from '@deck.gl/aggregation-layers'; +import {WebGLAggregator} from '@deck.gl/aggregation-layers'; import {device} from '@deck.gl/test-utils'; import {IncomeSurvey} from './data-sample'; import {getResourceCounts, binaryAttributeToArray} from './test-utils'; -test('GPUAggregator#resources', t => { +test('WebGLAggregator#resources', t => { const oldResourceCounts = getResourceCounts(); // An aggregator that calculates average income grouped by education - const aggregator = new GPUAggregator(device, { + const aggregator = new WebGLAggregator(device, { dimensions: 1, - numChannels: 1, + channelCount: 1, bufferLayout: [ {name: 'income', format: 'float32', stepMode: 'vertex'}, {name: 'education', format: 'float32', stepMode: 'vertex'} @@ -73,12 +73,12 @@ test('GPUAggregator#resources', t => { t.end(); }); -test('GPUAggregator#1D', t => { +test('WebGLAggregator#1D', t => { // An aggregator that calculates: // [0] total count [1] average income [2] highest education, grouped by age - const aggregator = new GPUAggregator(device, { + const aggregator = new WebGLAggregator(device, { dimensions: 1, - numChannels: 3, + channelCount: 3, bufferLayout: [ {name: 'age', format: 'float32', stepMode: 'vertex'}, {name: 'income', format: 'float32', stepMode: 'vertex'}, @@ -124,17 +124,17 @@ test('GPUAggregator#1D', t => { aggregator.update(); aggregator.preDraw({moduleSettings: {}}); - t.is(aggregator.numBins, 15, 'numBins'); + t.is(aggregator.binCount, 15, 'binCount'); t.deepEqual( - binaryAttributeToArray(aggregator.getBins(), aggregator.numBins), + binaryAttributeToArray(aggregator.getBins(), aggregator.binCount), // prettier-ignore [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], 'getBins()' ); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(0), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(0), aggregator.binCount), // prettier-ignore [NaN, 1, 5, 5, 3, 2, 3, 2, 2, 2, 1, 2, 1, 1, 2], 'getResult() - total counts' @@ -142,7 +142,7 @@ test('GPUAggregator#1D', t => { t.deepEqual(aggregator.getResultDomain(0), [1, 5], 'getResultDomain() - counts'); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(1), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(1), aggregator.binCount), // prettier-ignore [NaN, 25, 48, 54, 100, 145, 250, 72.5, 252.5, 107.5, 0, 127.5, 0, 40, 25], 'getResult() - mean income' @@ -150,7 +150,7 @@ test('GPUAggregator#1D', t => { t.deepEqual(aggregator.getResultDomain(1), [0, 252.5], 'getResultDomain() - mean income'); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(2), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(2), aggregator.binCount), // prettier-ignore [NaN, 1, 3, 4, 5, 4, 5, 3, 3, 5, 3, 4, 1, 2, 3], 'getResult() - max education' @@ -175,12 +175,12 @@ test('GPUAggregator#1D', t => { t.end(); }); -test('GPUAggregator#2D', t => { +test('WebGLAggregator#2D', t => { // An aggregator that calculates: // [0] total count [1] average income, grouped by [age, education] - const aggregator = new GPUAggregator(device, { + const aggregator = new WebGLAggregator(device, { dimensions: 2, - numChannels: 2, + channelCount: 2, bufferLayout: [ {name: 'age', format: 'float32', stepMode: 'vertex'}, {name: 'income', format: 'float32', stepMode: 'vertex'}, @@ -230,10 +230,10 @@ test('GPUAggregator#2D', t => { aggregator.update(); aggregator.preDraw({moduleSettings: {}}); - t.is(aggregator.numBins, 20, 'numBins'); + t.is(aggregator.binCount, 20, 'binCount'); t.deepEqual( - binaryAttributeToArray(aggregator.getBins(), aggregator.numBins), + binaryAttributeToArray(aggregator.getBins(), aggregator.binCount), // prettier-ignore [ 2, 1, 3, 1, 4, 1, 5, 1, 2, 2, 3, 2, 4, 2, 5, 2, @@ -244,7 +244,7 @@ test('GPUAggregator#2D', t => { ); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(0), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(0), aggregator.binCount), // prettier-ignore [ NaN, NaN, NaN, NaN, 4, NaN, 1, NaN, 4, 2, @@ -255,7 +255,7 @@ test('GPUAggregator#2D', t => { t.deepEqual(aggregator.getResultDomain(0), [1, 4], 'getResultDomain() - counts'); t.deepEqual( - binaryAttributeToArray(aggregator.getResult(1), aggregator.numBins), + binaryAttributeToArray(aggregator.getResult(1), aggregator.binCount), // prettier-ignore [ NaN, NaN, NaN, NaN, 25, NaN, 65, NaN, 97.5, 90, diff --git a/test/modules/aggregation-layers/index.ts b/test/modules/aggregation-layers/index.ts index 2203da51cfa..6713fe7b763 100644 --- a/test/modules/aggregation-layers/index.ts +++ b/test/modules/aggregation-layers/index.ts @@ -41,4 +41,4 @@ import './utils/color-utils.spec'; // import './utils/gpu-grid-aggregator.spec'; import './utils/scale-utils.spec'; -import './aggregation-layer-v9/gpu-aggregator.spec'; +import './aggregation-layer-v9/webgl-aggregator.spec';