From 39b105f69907c8507582f8b649b1481d86e547d3 Mon Sep 17 00:00:00 2001 From: 1chandu Date: Fri, 6 Dec 2019 17:17:07 -0800 Subject: [PATCH] [PART-3] CPUAggregation Refactor : Remove CPU Aggregation from GPUGridAggregator (#3953) * GPUAggregator: Remove CPU Aggregation support * Consolidate CPU Aggregation * ContourLayer: only perform shallow compare for contours prop, individual triggers not needed anymore * Conver matrix multiply to translation/scaling * Remove fp64 flag and dependency on project64 module * Unify screenspace projections, remove usage of gridTransformMatrix * Use 3D position for CPU and GPU aggregation * GPUAggregation: use 64 bit position attribute --- .../v8.0/grid-aggregation-attributemanager.md | 2 +- .../RFCs/vNext/fixed-frame-coordinates-rfc.md | 3 +- docs/layers/contour-layer.md | 20 +- docs/layers/gpu-grid-layer.md | 23 +- .../src/aggregation-layer.js | 12 +- .../src/aggregation-layer.md | 35 ++ .../src/contour-layer/contour-layer.js | 249 ++++---- .../src/cpu-grid-layer/cpu-grid-layer.js | 2 +- .../src/cpu-grid-layer/grid-aggregator.js | 22 +- .../src/gpu-grid-layer/gpu-grid-layer.js | 194 +++--- .../src/grid-aggregation-layer.js | 301 ++++++++- .../src/grid-aggregation-layer.md | 33 + .../src/heatmap-layer/heatmap-layer.js | 11 +- .../src/hexagon-layer/hexagon-layer.js | 2 +- .../screen-grid-layer/screen-grid-layer.js | 170 ++--- ...vs-64.glsl.js => aggregate-all-vs.glsl.js} | 0 .../aggregate-to-grid-vs-64.glsl.js | 85 --- .../aggregate-to-grid-vs.glsl.js | 29 +- .../gpu-grid-aggregator-constants.js | 4 - .../gpu-grid-aggregator.js | 580 +++--------------- .../gpu-grid-aggregator.md | 25 +- .../grid-aggregation-utils.js | 198 ------ .../src/utils/grid-aggregation-utils.js | 47 ++ test/browser.js | 3 +- test/data/grid-aggregation-data.js | 11 +- .../aggregation-layer.spec.js | 16 +- .../contour-layer/contour-layer.spec.js | 123 ++-- .../gpu-grid-layer/gpu-grid-layer.spec.js | 39 +- .../grid-aggregation-layer.spec.js | 278 +++++++++ test/modules/aggregation-layers/index.js | 1 - .../screen-grid-layer.spec.js | 11 +- .../utils/gpu-grid-aggregator.spec.js | 330 +++------- .../utils/grid-aggregation-utils.spec.js | 70 +-- test/render/test-cases.js | 13 - 34 files changed, 1302 insertions(+), 1640 deletions(-) create mode 100644 modules/aggregation-layers/src/aggregation-layer.md create mode 100644 modules/aggregation-layers/src/grid-aggregation-layer.md rename modules/aggregation-layers/src/utils/gpu-grid-aggregation/{aggregate-all-vs-64.glsl.js => aggregate-all-vs.glsl.js} (100%) delete mode 100644 modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-to-grid-vs-64.glsl.js delete mode 100644 modules/aggregation-layers/src/utils/gpu-grid-aggregation/grid-aggregation-utils.js create mode 100644 modules/aggregation-layers/src/utils/grid-aggregation-utils.js create mode 100644 test/modules/aggregation-layers/grid-aggregation-layer.spec.js diff --git a/dev-docs/RFCs/v8.0/grid-aggregation-attributemanager.md b/dev-docs/RFCs/v8.0/grid-aggregation-attributemanager.md index f8c2a8aee23..3da3b0298f3 100644 --- a/dev-docs/RFCs/v8.0/grid-aggregation-attributemanager.md +++ b/dev-docs/RFCs/v8.0/grid-aggregation-attributemanager.md @@ -28,7 +28,7 @@ Introduce new composite layer `AggregationLayer`, which owns `GPUGridAggregator` #### Attributes setup -`AttributeManager` is only available for `Layer` and it's subclasses and not available for a `CompositeLayer`. We can create an instance of `AttributeManger` in `AggregationLayer` and provide it to all aggregation layers which can be used for all offline renderings performed for aggregation. +`AttributeManager` is only available for `Layer` and it's subclasses and not available for a `CompositeLayer`. We can create an instance of `AttributeManager` in `AggregationLayer` and provide it to all aggregation layers which can be used for all offline renderings performed for aggregation. #### Model setup diff --git a/dev-docs/RFCs/vNext/fixed-frame-coordinates-rfc.md b/dev-docs/RFCs/vNext/fixed-frame-coordinates-rfc.md index a731a93fbec..40dd505e21b 100644 --- a/dev-docs/RFCs/vNext/fixed-frame-coordinates-rfc.md +++ b/dev-docs/RFCs/vNext/fixed-frame-coordinates-rfc.md @@ -19,7 +19,7 @@ However, fixed frame geospatial coordinates are also well-defined and have a com During our collaboration with the Cesium team around 3D tiles in loaders.gl and math.gl, we had to implement support for fixed frame geospatial coordinates in math.gl (for Ellipsoid math and WGS84 transformations from cartesian to cartographic coordinates). -This puts us in a position where we now have both the necessary support libraries, an initial use case for, and a more mature understanding of fixed frame geospatial coodinates, so it makes sense to create an RFC to dig into what adding fixed frame coordinate support directly to deck.gl would mean. +This puts us in a position where we now have both the necessary support libraries, an initial use case for, and a more mature understanding of fixed frame geospatial coordinates, so it makes sense to create an RFC to dig into what adding fixed frame coordinate support directly to deck.gl would mean. ## Overview @@ -112,4 +112,3 @@ new Layer({ ### Proposal: Built-in Frustum Culling Functionality in deck.gl Would be easier to implement in a fixed coordinate system. - diff --git a/docs/layers/contour-layer.md b/docs/layers/contour-layer.md index 64a6e4df776..90bc2e80a4d 100644 --- a/docs/layers/contour-layer.md +++ b/docs/layers/contour-layer.md @@ -14,6 +14,12 @@ import DeckGL from '@deck.gl/react'; import {ContourLayer} from '@deck.gl/aggregation-layers'; +const CONTOURS = [ + {threshold: 1, color: [255, 0, 0, 255], strokeWidth: 1}, // => Isoline for threshold 1 + {threshold: 5, color: [0, 255, 0], strokeWidth: 2}, // => Isoline for threshold 5 + {threshold: [6, 10], color: [0, 0, 255, 128]} // => Isoband for threshold range [6, 10) +]; + const App = ({data, viewport}) => { /** @@ -26,11 +32,7 @@ const App = ({data, viewport}) => { const layer = new ContourLayer({ id: 'contourLayer', // Three contours are rendered. - contours: [ - {threshold: 1, color: [255, 0, 0, 255], strokeWidth: 1}, // => Isoline for threshold 1 - {threshold: 5, color: [0, 255, 0], strokeWidth: 2}, // => Isoline for threshold 5 - {threshold: [6, 10], color: [0, 0, 255, 128]} // => Isoband for threshold range [6, 10) - ], + contours: CONTOURS, cellSize: 200, getPosition: d => d.COORDINATES, }); @@ -107,18 +109,14 @@ Array of objects with following keys * `zIndex` (Number, optional) : Defines z order of the contour. Contour with higher `zIndex` value is rendered above contours with lower `zIndex` values. When visualizing overlapping contours, `zIndex` along with `zOffset` (defined below) can be used to precisely layout contours. This also avoids z-fighting rendering issues. If not specified a unique value from `0` to `n` (number of contours) is assigned. +NOTE: Like any other layer prop, a shallow comparison is performed on `contours` prop to determine if it is changed. This prop should be set to an array object, that changes only when contours need to be changed. + ##### `zOffset` (Number, optional) * Default: `0.005` A very small z offset that is added for each vertex of a contour (Isoline or Isoband). This is needed to control the layout of contours, especially when rendering overlapping contours. Imagine a case where an Isoline is specified which is overlapped with an Isoband. To make sure the Isoline is visible we need to render this above the Isoband. -##### `fp64` (Boolean, optional) - -* Default: `false` - -Whether the aggregation should be performed in high-precision 64-bit mode. Note that since deck.gl v6.1, the default 32-bit projection uses a hybrid mode that matches 64-bit precision with significantly better performance. - ### Data Accessors ##### `getPosition` ([Function](/docs/developer-guide/using-layers.md#accessors), optional) diff --git a/docs/layers/gpu-grid-layer.md b/docs/layers/gpu-grid-layer.md index be19268d6c2..0ac1e272aec 100644 --- a/docs/layers/gpu-grid-layer.md +++ b/docs/layers/gpu-grid-layer.md @@ -8,7 +8,7 @@ # GPUGridLayer (WebGL2) The `GPUGridLayer` renders a grid heatmap based on an array of points. -It takes the constant cell size, aggregates input points into cells. When possible aggregation is performed on GPU. The color +It takes the constant cell size, aggregates input points into cells. This layer performs aggregation on GPU hence not supported in non WebGL2 browsers. The color and height of the cell is scaled by number of points it contains. `GPUGridLayer` is one of the sublayers for [GridLayer](/docs/layers/grid-layer.md) and is only supported when using `WebGL2` enabled browsers. It is provided to customize GPU Aggregation for advanced use cases. For any regular use case, [GridLayer](/docs/layers/grid-layer.md) is recommended. @@ -30,7 +30,6 @@ const App = ({data, viewport}) => { */ const layer = new GPUGridLayer({ id: 'gpu-grid-layer', - gpuAggregation: true, data, pickable: true, extruded: true, @@ -51,7 +50,9 @@ const App = ({data, viewport}) => { **Note:** The `GPUGridLayer` at the moment only works with `COORDINATE_SYSTEM.LNGLAT`. -**Note:** This layer is similar to [CPUGridLayer](/docs/layers/cpu-grid-layer.md) but supports aggregation on GPU when possible. Check below for more detailed differences of this layer compared to `CPUGridLayer`. +**Note:** GPU Aggregation is faster only when using large data sets (point count is more than 500K), for smaller data sets GPU Aggregation could be potentially slower than CPU Aggregation. + +**Note:** This layer is similar to [CPUGridLayer](/docs/layers/cpu-grid-layer.md) but performs aggregation on GPU. Check below for more detailed differences of this layer compared to `CPUGridLayer`. ## Installation @@ -137,22 +138,6 @@ Cell elevation multiplier. The elevation of cell is calculated by Whether to enable cell elevation. Cell elevation scale by count of points in each cell. If set to false, all cell will be flat. -##### `fp64` (Boolean, optional) - -* Default: `false` - -Whether the aggregation should be performed in high-precision 64-bit mode. Note that since deck.gl v6.1, the default 32-bit projection uses a hybrid mode that matches 64-bit precision with significantly better performance. - -##### `gpuAggregation` (bool, optional) - -* Default: true - -When set to true and browser supports GPU aggregation, aggregation is performed on GPU. GPU aggregation can be 2 to 3 times faster depending upon number of points and number of cells. - -**Note:** GPU Aggregation requires WebGL2. When `gpuAggregation` is set to true and browser doesn't support WebGL2, aggregation falls back to CPU. - -**Note:** GPU Aggregation is faster only when using large data sets (point count is more than 500K), for smaller data sets GPU Aggregation could be potentially slower than CPU Aggregation. - ##### `material` (Object, optional) * Default: `true` diff --git a/modules/aggregation-layers/src/aggregation-layer.js b/modules/aggregation-layers/src/aggregation-layer.js index b7774fc260b..2e160bd4682 100644 --- a/modules/aggregation-layers/src/aggregation-layer.js +++ b/modules/aggregation-layers/src/aggregation-layer.js @@ -48,7 +48,7 @@ export default class AggregationLayer extends CompositeLayer { if (shaders && shaders.defines) { shaders.defines.NON_INSTANCED_MODEL = 1; } - this._updateShaders(shaders); + this.updateShaders(shaders); } // Explictly call to update attributes as 'CompositeLayer' doesn't call this @@ -83,7 +83,11 @@ export default class AggregationLayer extends CompositeLayer { return moduleSettings; } - _isAggregationDirty(opts) { + updateShaders(shaders) { + // Default implemention is empty, subclasses can update their Model objects if needed + } + + isAggregationDirty(opts) { if (this.state.dataChanged || opts.changeFlags.extensionsChanged) { return true; } @@ -99,9 +103,7 @@ export default class AggregationLayer extends CompositeLayer { ); } - _updateShaders(shaders) { - // Default implemention is empty, subclasses can update their Model objects if needed - } + // Private // override Composite layer private method to create AttributeManager instance _getAttributeManager() { diff --git a/modules/aggregation-layers/src/aggregation-layer.md b/modules/aggregation-layers/src/aggregation-layer.md new file mode 100644 index 00000000000..617536ec771 --- /dev/null +++ b/modules/aggregation-layers/src/aggregation-layer.md @@ -0,0 +1,35 @@ +# AggregationLayer + +All of the layers in `@deck.gl/aggregation-layers` module perform some sort of data aggregation. All these layers perform aggregation with different parameters (CPU vs GPU, aggregation to rectangular bins vs hexagon bins, world space vs screen space, aggregation of single weight vs multiple weights etc). + +`AggregationLayer` and `GridAggregationLayer` perform most of the common tasks for aggregation with flexibility of customizations. This document describes what `AggregationLayer` does and how to use it in other aggregation layers. + + +`AggregationLayer` is subclassed form `CompositeLayer` and all layers in `@deck.gl/aggregation-layers` are subclassed from this Layer. + +### Integration with `AttributeManager` + +This layer creates `AttributeManager` and makes it available for its subclasses. Any aggregation layer can add attributes to the `AttributeManager` and retrieve them using `getAttributes` method. This enables using `AttributeManager`'s features and optimization for using attributes. Also manual iteration of `data` prop can be removed and attributes can be directly set on GPU aggregation models or accessed directly for CPU aggregation. + +Example: Adding attributes to an aggregation layer + +``` +const attributeManager = this.getAttributeManager(); +attributeManager.add({ + positions: {size: 3, accessor: 'getPosition'}, + color: {size: 3, accessor: 'getColorWeight'}, + elevation: {size: 3, accessor: 'getElevationWeight'} +}); +``` + +### updateState() + +During update state, Subclasses of `AggregationLayer` must first call 'super.updateState()', which calls + +- `updateShaders(shaders)` : Subclasses can override this if they need to update shaders, for example, when performing GPU aggregation, aggregation shaders must be merged with argument of this function to correctly apply `extensions`. + +- `_updateAttributes`: This checks and updates attributes based on updated props. + +### Checking if aggregation is dirty + +Constructor, takes an array of props, `aggregationProps`, and a private method `isAggregationDirty()` is provided that returns `true` when any of the props in `aggregationProps` are changed. Subclasses can customize this to desired props by providing `aggregatinProps` array. diff --git a/modules/aggregation-layers/src/contour-layer/contour-layer.js b/modules/aggregation-layers/src/contour-layer/contour-layer.js index fb78ffbf090..1191d795c06 100644 --- a/modules/aggregation-layers/src/contour-layer/contour-layer.js +++ b/modules/aggregation-layers/src/contour-layer/contour-layer.js @@ -19,12 +19,13 @@ // THE SOFTWARE. import {equals} from 'math.gl'; -import {LineLayer, SolidPolygonLayer} from '@deck.gl/layers'; import GL from '@luma.gl/constants'; +import {LineLayer, SolidPolygonLayer} from '@deck.gl/layers'; import {generateContours} from './contour-utils'; +import {log} from '@deck.gl/core'; import GPUGridAggregator from '../utils/gpu-grid-aggregation/gpu-grid-aggregator'; -import {pointToDensityGridData} from '../utils/gpu-grid-aggregation/grid-aggregation-utils'; +import {AGGREGATION_OPERATION} from '../utils/aggregation-operation-utils'; import GridAggregationLayer from '../grid-aggregation-layer'; const DEFAULT_COLOR = [255, 255, 255, 255]; @@ -37,28 +38,38 @@ const defaultProps = { getPosition: {type: 'accessor', value: x => x.position}, getWeight: {type: 'accessor', value: x => 1}, gpuAggregation: true, + aggregation: 'SUM', // contour lines contours: [{threshold: DEFAULT_THRESHOLD}], - fp64: false, zOffset: 0.005 }; // props , when changed requires re-aggregation -const AGGREGATION_PROPS = ['gpuAggregation']; +const AGGREGATION_PROPS = ['aggregation', 'getWeight']; export default class ContourLayer extends GridAggregationLayer { initializeState() { - super.initializeState(AGGREGATION_PROPS); + super.initializeState({aggregationProps: AGGREGATION_PROPS}); this.setState({ contourData: {}, - colorTrigger: 0, - strokeWidthTrigger: 0 + weights: { + count: { + size: 1, + operation: AGGREGATION_OPERATION.SUM + } + } }); const attributeManager = this.getAttributeManager(); attributeManager.add({ - positions: {size: 3, accessor: 'getPosition', type: GL.DOUBLE, fp64: false}, + positions: { + size: 3, + accessor: 'getPosition', + type: GL.DOUBLE, + fp64: this.use64bitPositions() + }, + // this attribute is used in gpu aggregation path only count: {size: 3, accessor: 'getWeight'} }); } @@ -66,131 +77,124 @@ export default class ContourLayer extends GridAggregationLayer { updateState(opts) { super.updateState(opts); let contoursChanged = false; - const dataChanged = this._isAggregationDirty(opts); - const cellSizeChanged = opts.oldProps.cellSize !== opts.props.cellSize; - if (dataChanged || cellSizeChanged) { - this.setState({countsData: null}); - this._aggregateData({ - dataChanged, - cellSizeChanged - }); - } - if (this._shouldRebuildContours(opts)) { + const {oldProps, props} = opts; + const {aggregationDirty} = this.state; + + if (oldProps.contours !== props.contours || oldProps.zOffset !== props.zOffset) { contoursChanged = true; this._updateThresholdData(opts.props); } - if (dataChanged || cellSizeChanged || contoursChanged) { + + if (this.getNumInstances() > 0 && (aggregationDirty || contoursChanged)) { this._generateContours(); - } else { - // data for sublayers not changed check if color or strokeWidth need to be updated - this._updateSubLayerTriggers(opts.oldProps, opts.props); } } renderLayers() { const {contourSegments, contourPolygons} = this.state.contourData; - const hasIsolines = contourSegments && contourSegments.length > 0; - const hasIsobands = contourPolygons && contourPolygons.length > 0; - const lineLayer = hasIsolines && new LineLayer(this._getLineLayerProps()); - const solidPolygonLayer = - hasIsobands && new SolidPolygonLayer(this._getSolidPolygonLayerProps()); - return [lineLayer, solidPolygonLayer]; + const LinesSubLayerClass = this.getSubLayerClass('lines', LineLayer); + const BandsSubLayerClass = this.getSubLayerClass('bands', SolidPolygonLayer); + + // Contour lines layer + const lineLayer = + contourSegments && + contourSegments.length > 0 && + new LinesSubLayerClass( + this.getSubLayerProps({ + id: 'contour-line-layer' + }), + { + data: this.state.contourData.contourSegments, + getSourcePosition: d => d.start, + getTargetPosition: d => d.end, + getColor: this._onGetSublayerColor.bind(this), + getWidth: this._onGetSublayerStrokeWidth.bind(this) + } + ); + + // Contour bands layer + const bandsLayer = + contourPolygons && + contourPolygons.length > 0 && + new BandsSubLayerClass( + this.getSubLayerProps({ + id: 'contour-solid-polygon-layer' + }), + { + data: this.state.contourData.contourPolygons, + getPolygon: d => d.vertices, + getFillColor: this._onGetSublayerColor.bind(this) + } + ); + + return [lineLayer, bandsLayer]; } - // Private - - _aggregateData(aggregationFlags) { - const { - data, - cellSize: cellSizeMeters, - getWeight, - gpuAggregation, - fp64, - coordinateSystem - } = this.props; - const {gpuGridAggregator} = this.state; - - const {weights, gridSize, gridOrigin, cellSize, boundingBox} = pointToDensityGridData({ - data, - cellSizeMeters, - weightParams: {count: {getWeight}}, - gpuAggregation, - gpuGridAggregator, - fp64, - coordinateSystem, - viewport: this.context.viewport, - boundingBox: this.state.boundingBox, // avoid parsing data when it is not changed. - aggregationFlags, - vertexCount: this.getNumInstances(), - attributes: this.getAttributes(), - moduleSettings: this.getModuleSettings() - }); + // Aggregation Overrides + updateAggregationFlags({props, oldProps}) { + const cellSizeChanged = oldProps.cellSize !== props.cellSize; + let gpuAggregation = props.gpuAggregation; + if (this.state.gpuAggregation !== props.gpuAggregation) { + if (gpuAggregation && !GPUGridAggregator.isSupported(this.context.gl)) { + log.warn('GPU Grid Aggregation not supported, falling back to CPU')(); + gpuAggregation = false; + } + } + const gpuAggregationChanged = gpuAggregation !== this.state.gpuAggregation; + // Consider switching between CPU and GPU aggregation as data changed as it requires + // re aggregation. + const dataChanged = this.state.dataChanged || gpuAggregationChanged; this.setState({ - countsData: weights.count.aggregationData, - countsBuffer: weights.count.aggregationBuffer, - gridSize, - gridOrigin, - cellSize, - boundingBox + dataChanged, + cellSizeChanged, + cellSize: props.cellSize, + needsReProjection: dataChanged || cellSizeChanged, + gpuAggregation }); } + // Private (Contours) + _generateContours() { - const {gridSize, gridOrigin, cellSize, thresholdData} = this.state; - let {countsData} = this.state; - if (!countsData) { - const {countsBuffer} = this.state; - countsData = countsBuffer.getData(); - this.setState({countsData}); + const {numCol, numRow, boundingBox, gridOffset, thresholdData} = this.state; + const {count} = this.state.weights; + let {aggregationData} = count; + if (!aggregationData) { + aggregationData = count.aggregationBuffer.getData(); + count.aggregationData = aggregationData; } - const {cellWeights} = GPUGridAggregator.getCellData({countsData}); - // const thresholds = this.props.contours.map(x => x.threshold); + const {cellWeights} = GPUGridAggregator.getCellData({countsData: aggregationData}); const contourData = generateContours({ thresholdData, cellWeights, - gridSize, - gridOrigin, - cellSize + gridSize: [numCol, numRow], + gridOrigin: [boundingBox.xMin, boundingBox.yMin], + cellSize: [gridOffset.xOffset, gridOffset.yOffset] }); // contourData contains both iso-lines and iso-bands if requested. this.setState({contourData}); } - _getLineLayerProps() { - const {colorTrigger, strokeWidthTrigger} = this.state; - - return this.getSubLayerProps({ - id: 'contour-line-layer', - data: this.state.contourData.contourSegments, - getSourcePosition: d => d.start, - getTargetPosition: d => d.end, - getColor: this._onGetSublayerColor.bind(this), - getWidth: this._onGetSublayerStrokeWidth.bind(this), - widthUnits: 'pixels', - updateTriggers: { - getColor: colorTrigger, - getWidth: strokeWidthTrigger - } - }); + _updateThresholdData(props) { + const {contours, zOffset} = props; + const count = contours.length; + const thresholdData = new Array(count); + for (let i = 0; i < count; i++) { + const {threshold, zIndex} = contours[i]; + thresholdData[i] = { + threshold, + zIndex: zIndex || i, + zOffset + }; + } + this.setState({thresholdData}); } - _getSolidPolygonLayerProps() { - const {colorTrigger} = this.state; - - return this.getSubLayerProps({ - id: 'contour-solid-polygon-layer', - data: this.state.contourData.contourPolygons, - getPolygon: d => d.vertices, - getFillColor: this._onGetSublayerColor.bind(this), - updateTriggers: { - getFillColor: colorTrigger - } - }); - } + // Private (Sublayers) _onGetSublayerColor(element) { // element is either a line segment or polygon @@ -217,47 +221,6 @@ export default class ContourLayer extends GridAggregationLayer { }); return strokeWidth; } - - _shouldRebuildContours({oldProps, props}) { - if ( - !oldProps.contours || - !oldProps.zOffset || - oldProps.contours.length !== props.contours.length || - oldProps.zOffset !== props.zOffset - ) { - return true; - } - const oldThresholds = oldProps.contours.map(x => x.threshold); - const thresholds = props.contours.map(x => x.threshold); - - return thresholds.some((_, i) => !equals(thresholds[i], oldThresholds[i])); - } - - _updateSubLayerTriggers(oldProps, props) { - if (oldProps && oldProps.contours && props && props.contours) { - if (props.contours.some((contour, i) => contour.color !== oldProps.contours[i].color)) { - this.state.colorTrigger++; - } - if ( - props.contours.some( - (contour, i) => contour.strokeWidth !== oldProps.contours[i].strokeWidth - ) - ) { - this.state.strokeWidthTrigger++; - } - } - } - - _updateThresholdData(props) { - const thresholdData = props.contours.map((x, index) => { - return { - threshold: x.threshold, - zIndex: x.zIndex || index, - zOffset: props.zOffset - }; - }); - this.setState({thresholdData}); - } } ContourLayer.layerName = 'ContourLayer'; diff --git a/modules/aggregation-layers/src/cpu-grid-layer/cpu-grid-layer.js b/modules/aggregation-layers/src/cpu-grid-layer/cpu-grid-layer.js index 0b0dc5d7c0f..7dcbcbbf5a4 100644 --- a/modules/aggregation-layers/src/cpu-grid-layer/cpu-grid-layer.js +++ b/modules/aggregation-layers/src/cpu-grid-layer/cpu-grid-layer.js @@ -76,7 +76,7 @@ export default class CPUGridLayer extends AggregationLayer { }; const attributeManager = this.getAttributeManager(); attributeManager.add({ - positions: {size: 3, accessor: 'getPosition' /* , type: GL.DOUBLE, fp64: false*/} + positions: {size: 3, accessor: 'getPosition'} }); // color and elevation attributes can't be added as attributes // they are calcualted using 'getValue' accessor that takes an array of pints. diff --git a/modules/aggregation-layers/src/cpu-grid-layer/grid-aggregator.js b/modules/aggregation-layers/src/cpu-grid-layer/grid-aggregator.js index 58f53da806b..153c6fccfd9 100644 --- a/modules/aggregation-layers/src/cpu-grid-layer/grid-aggregator.js +++ b/modules/aggregation-layers/src/cpu-grid-layer/grid-aggregator.js @@ -82,21 +82,19 @@ function pointsToGridHashing(props, aggregationParams) { const gridHash = {}; const {iterable, objectInfo} = createIterable(data); + const position = new Array(3); for (const pt of iterable) { objectInfo.index++; - let lng = positions[objectInfo.index * size]; - let lat = positions[objectInfo.index * size + 1]; - - if (projectPoints) { - [lng, lat] = viewport.project([lng, lat]); - } - - if (Number.isFinite(lat) && Number.isFinite(lng)) { - const latIdx = Math.floor((lat + offsets[1]) / gridOffset.yOffset); - const lonIdx = Math.floor((lng + offsets[0]) / gridOffset.xOffset); - const key = `${latIdx}-${lonIdx}`; + position[0] = positions[objectInfo.index * size]; + position[1] = positions[objectInfo.index * size + 1]; + position[2] = size >= 3 ? positions[objectInfo.index * size + 2] : 0; + const [x, y] = projectPoints ? viewport.project(position) : position; + if (Number.isFinite(x) && Number.isFinite(y)) { + const yIndex = Math.floor((y + offsets[1]) / gridOffset.yOffset); + const xIndex = Math.floor((x + offsets[0]) / gridOffset.xOffset); + const key = `${yIndex}-${xIndex}`; - gridHash[key] = gridHash[key] || {count: 0, points: [], lonIdx, latIdx}; + gridHash[key] = gridHash[key] || {count: 0, points: [], lonIdx: xIndex, latIdx: yIndex}; gridHash[key].count += 1; gridHash[key].points.push(pt); } diff --git a/modules/aggregation-layers/src/gpu-grid-layer/gpu-grid-layer.js b/modules/aggregation-layers/src/gpu-grid-layer/gpu-grid-layer.js index 0799091ddb6..2e113b33655 100644 --- a/modules/aggregation-layers/src/gpu-grid-layer/gpu-grid-layer.js +++ b/modules/aggregation-layers/src/gpu-grid-layer/gpu-grid-layer.js @@ -18,12 +18,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import {Buffer} from '@luma.gl/core'; import GL from '@luma.gl/constants'; import {log} from '@deck.gl/core'; import GPUGridAggregator from '../utils/gpu-grid-aggregation/gpu-grid-aggregator'; import {AGGREGATION_OPERATION} from '../utils/aggregation-operation-utils'; -import {pointToDensityGridData} from '../utils/gpu-grid-aggregation/grid-aggregation-utils'; import {defaultColorRange, colorRangeToFlatArray} from '../utils/color-utils'; import GPUGridCellLayer from './gpu-grid-cell-layer'; import {pointToDensityGridDataCPU} from './../cpu-grid-layer/grid-aggregator'; @@ -48,17 +48,13 @@ const defaultProps = { coverage: {type: 'number', min: 0, max: 1, value: 1}, getPosition: {type: 'accessor', value: x => x.position}, extruded: false, - fp64: false, // Optional material for 'lighting' shader module - material: true, - - // GPU Aggregation - gpuAggregation: true + material: true }; // props , when changed requires re-aggregation -const AGGREGATION_PROPS = ['gpuAggregation', 'colorAggregation', 'elevationAggregation']; +const AGGREGATION_PROPS = ['colorAggregation', 'elevationAggregation']; export default class GPUGridLayer extends GridAggregationLayer { initializeState() { @@ -67,11 +63,39 @@ export default class GPUGridLayer extends GridAggregationLayer { if (!isSupported) { log.error('GPUGridLayer is not supported on this browser, use GridLayer instead')(); } - super.initializeState(AGGREGATION_PROPS); - this.setState({isSupported}); + super.initializeState({aggregationProps: AGGREGATION_PROPS}); + this.setState({ + gpuAggregation: true, + isSupported, + weights: { + color: { + needMin: true, + needMax: true, + combineMaxMin: true, + maxMinBuffer: new Buffer(gl, { + byteLength: 4 * 4, + accessor: {size: 4, type: GL.FLOAT, divisor: 1} + }) + }, + elevation: { + needMin: true, + needMax: true, + combineMaxMin: true, + maxMinBuffer: new Buffer(gl, { + byteLength: 4 * 4, + accessor: {size: 4, type: GL.FLOAT, divisor: 1} + }) + } + } + }); const attributeManager = this.getAttributeManager(); attributeManager.add({ - positions: {size: 3, accessor: 'getPosition', type: GL.DOUBLE, fp64: false}, + positions: { + size: 3, + accessor: 'getPosition', + type: GL.DOUBLE, + fp64: this.use64bitPositions() + }, color: {size: 3, accessor: 'getColorWeight'}, elevation: {size: 3, accessor: 'getElevationWeight'} }); @@ -83,14 +107,8 @@ export default class GPUGridLayer extends GridAggregationLayer { return; } super.updateState(opts); - const dataChanged = this._isAggregationDirty(opts); - const cellSizeChanged = opts.oldProps.cellSize !== opts.props.cellSize; - if (dataChanged || cellSizeChanged) { - this._aggregateData({ - dataChanged, - cellSizeChanged - }); - + const {aggregationDirty} = this.state; + if (aggregationDirty) { // reset cached CPU Aggregation results (used for picking) this.setState({ gridHash: null @@ -99,7 +117,11 @@ export default class GPUGridLayer extends GridAggregationLayer { } getHashKeyForIndex(index) { - const {gridSize, gridOrigin, cellSize} = this.state; + const {numRow, numCol, boundingBox, gridOffset} = this.state; + const gridSize = [numCol, numRow]; + const gridOrigin = [boundingBox.xMin, boundingBox.yMin]; + const cellSize = [gridOffset.xOffset, gridOffset.yOffset]; + const yIndex = Math.floor(index / gridSize[0]); const xIndex = index - yIndex * gridSize[0]; // This will match the index to the hash-key to access aggregation data from CPU aggregation results. @@ -113,7 +135,11 @@ export default class GPUGridLayer extends GridAggregationLayer { } getPositionForIndex(index) { - const {gridSize, gridOrigin, cellSize} = this.state; + const {numRow, numCol, boundingBox, gridOffset} = this.state; + const gridSize = [numCol, numRow]; + const gridOrigin = [boundingBox.xMin, boundingBox.yMin]; + const cellSize = [gridOffset.xOffset, gridOffset.yOffset]; + const yIndex = Math.floor(index / gridSize[0]); const xIndex = index - yIndex * gridSize[0]; const yPos = yIndex * cellSize[1] + gridOrigin[1]; @@ -146,10 +172,17 @@ export default class GPUGridLayer extends GridAggregationLayer { const {props} = this; let {gridHash} = this.state; if (!gridHash) { + const {gridOffset, cellOffset, width, height, boundingBox} = this.state; + const {viewport} = this.context; + const attributes = this.getAttributes(); const cpuAggregation = pointToDensityGridDataCPU(props, { - viewport: this.context.viewport, - attributes: this.getAttributes(), - numInstances: this.getNumInstances(props) + gridOffset, + width, + height, + attributes, + viewport, + cellOffset, + boundingBox }); gridHash = cpuAggregation.gridHash; this.setState({gridHash}); @@ -167,53 +200,6 @@ export default class GPUGridLayer extends GridAggregationLayer { }); } - _aggregateData(aggregationFlags) { - const { - data, - cellSize: cellSizeMeters, - gpuAggregation, - getColorWeight, - colorAggregation, - getElevationWeight, - elevationAggregation, - fp64 - } = this.props; - const weightParams = { - color: { - getWeight: getColorWeight, - operation: - AGGREGATION_OPERATION[colorAggregation] || - AGGREGATION_OPERATION[defaultProps.colorAggregation], - needMin: true, - needMax: true, - combineMaxMin: true - }, - elevation: { - getWeight: getElevationWeight, - operation: - AGGREGATION_OPERATION[elevationAggregation] || - AGGREGATION_OPERATION[defaultProps.elevationAggregation], - needMin: true, - needMax: true, - combineMaxMin: true - } - }; - const {weights, gridSize, gridOrigin, cellSize, boundingBox} = pointToDensityGridData({ - data, - cellSizeMeters, - weightParams, - gpuAggregation, - gpuGridAggregator: this.state.gpuGridAggregator, - boundingBox: this.state.boundingBox, // avoid parsing data when it is not changed. - aggregationFlags, - fp64, - vertexCount: this.getNumInstances(), - attributes: this.getAttributes(), - moduleSettings: this.getModuleSettings() - }); - this.setState({weights, gridSize, gridOrigin, cellSize, boundingBox}); - } - renderLayers() { if (!this.state.isSupported) { return null; @@ -229,7 +215,7 @@ export default class GPUGridLayer extends GridAggregationLayer { elevationDomain } = this.props; - const {weights, gridSize, gridOrigin, cellSize} = this.state; + const {weights, numRow, numCol, boundingBox, gridOffset} = this.state; const colorRange = colorRangeToFlatArray(this.props.colorRange); @@ -237,9 +223,9 @@ export default class GPUGridLayer extends GridAggregationLayer { return new SubLayerClass( { - gridSize, - gridOrigin, - gridOffset: cellSize, + gridSize: [numCol, numRow], + gridOrigin: [boundingBox.xMin, boundingBox.yMin], + gridOffset: [gridOffset.xOffset, gridOffset.yOffset], colorRange, elevationRange, colorDomain, @@ -256,10 +242,68 @@ export default class GPUGridLayer extends GridAggregationLayer { }), { data: weights, - numInstances: gridSize[0] * gridSize[1] + numInstances: numCol * numRow } ); } + + finalizeState() { + const {color, elevation} = this.state.weights; + [color, elevation].forEach(weight => { + const {aggregationBuffer, maxMinBuffer} = weight; + maxMinBuffer.delete(); + if (aggregationBuffer) { + aggregationBuffer.delete(); + } + }); + super.finalizeState(); + } + + // Aggregation Overrides + + updateWeightParams(opts) { + const {getColorWeight, colorAggregation, getElevationWeight, elevationAggregation} = opts.props; + const {color, elevation} = this.state.weights; + color.getWeight = getColorWeight; + color.operation = AGGREGATION_OPERATION[colorAggregation]; + elevation.getWeight = getElevationWeight; + elevation.operation = AGGREGATION_OPERATION[elevationAggregation]; + } + + allocateResources(numRow, numCol) { + if (this.state.numRow !== numRow || this.state.numCol !== numCol) { + const {color, elevation} = this.state.weights; + const dataBytes = numCol * numRow * 4 * 4; + const gl = this.context.gl; + updateAggregationBuffer(gl, color, dataBytes); + updateAggregationBuffer(gl, elevation, dataBytes); + } + } + + updateAggregationFlags(opts) { + const cellSizeChanged = opts.oldProps.cellSize !== opts.props.cellSize; + const {dataChanged} = this.state; + this.setState({ + cellSizeChanged, + cellSize: opts.props.cellSize, + needsReProjection: dataChanged || cellSizeChanged + }); + } +} + +// Helper methods +function updateAggregationBuffer(gl, weight, dataBytes) { + if (weight.aggregationBuffer) { + weight.aggregationBuffer.delete(); + } + weight.aggregationBuffer = new Buffer(gl, { + byteLength: dataBytes, + accessor: { + size: 4, + type: GL.FLOAT, + divisor: 1 + } + }); } GPUGridLayer.layerName = 'GPUGridLayer'; diff --git a/modules/aggregation-layers/src/grid-aggregation-layer.js b/modules/aggregation-layers/src/grid-aggregation-layer.js index 05e38b5a9db..542e5d4ac61 100644 --- a/modules/aggregation-layers/src/grid-aggregation-layer.js +++ b/modules/aggregation-layers/src/grid-aggregation-layer.js @@ -20,30 +20,317 @@ import AggregationLayer from './aggregation-layer'; import GPUGridAggregator from './utils/gpu-grid-aggregation/gpu-grid-aggregator'; +import {AGGREGATION_OPERATION, getValueFunc} from './utils/aggregation-operation-utils'; +import {Buffer} from '@luma.gl/core'; +import {log, COORDINATE_SYSTEM} from '@deck.gl/core'; +import GL from '@luma.gl/constants'; +import {getBoundingBox, alignToCell} from './utils/grid-aggregation-utils'; +import BinSorter from './utils/bin-sorter'; +import {pointToDensityGridDataCPU, getGridOffset} from './cpu-grid-layer/grid-aggregator'; export default class GridAggregationLayer extends AggregationLayer { - initializeState(aggregationProps) { + initializeState({aggregationProps, getCellSize}) { const {gl} = this.context; super.initializeState(aggregationProps); this.setState({ - gpuGridAggregator: new GPUGridAggregator(gl, {id: `${this.id}-gpu-aggregator`}) + // CPU aggregation results + layerData: {}, + gpuGridAggregator: new GPUGridAggregator(gl, {id: `${this.id}-gpu-aggregator`}), + cpuGridAggregator: pointToDensityGridDataCPU }); } + updateState(opts) { + // get current attributes + super.updateState(opts); + + this.updateAggregationFlags(opts); + // update bounding box and cellSize + this._updateProjectionParams(opts); + + let aggregationDirty = false; + const {needsReProjection, gpuAggregation} = this.state; + const needsReAggregation = this.isAggregationDirty(opts); + if (this.getNumInstances() <= 0) { + return; + } + // CPU aggregation is two steps + // 1. Create bins (based on cellSize and position) 2. Aggregate weights for each bin + // For GPU aggregation both above steps are combined into one step + + // step-1 + if (needsReProjection || (gpuAggregation && needsReAggregation)) { + this._updateAccessors(opts); + this._resetResults(); + this._updateAggregation(opts); + aggregationDirty = true; + } + // step-2 (Applicalbe for CPU aggregation only) + if (!gpuAggregation && (aggregationDirty || needsReAggregation)) { + this._resetResults(); + this._updateWeightBins(); + this._uploadAggregationResults(); + aggregationDirty = true; + } + + this.setState({aggregationDirty}); + } + finalizeState() { - super.finalizeState(); + const {count} = this.state.weights; + if (count && count.aggregationBuffer) { + count.aggregationBuffer.delete(); + } const {gpuGridAggregator} = this.state; if (gpuGridAggregator) { gpuGridAggregator.delete(); } + super.finalizeState(); + } + + // Must be implemented by subclasses + updateAggregationFlags(opts) { + // Sublayers should implement this method. + log.assert(false); + } + + // Methods that can be overriden by subclasses for customizations + + allocateResources(numRow, numCol) { + if (this.state.numRow !== numRow || this.state.numCol !== numCol) { + const {count} = this.state.weights; + const dataBytes = numCol * numRow * 4 * 4; + if (count.aggregationBuffer) { + count.aggregationBuffer.delete(); + } + count.aggregationBuffer = new Buffer(this.context.gl, { + byteLength: dataBytes, + accessor: { + size: 4, + type: GL.FLOAT, + divisor: 1 + } + }); + } + } + + updateResults({aggregationData, maxMinData, maxData, minData}) { + const {count} = this.state.weights; + if (count) { + count.aggregationData = aggregationData; + count.maxMinData = maxMinData; + count.maxData = maxData; + count.minData = minData; + } } - _updateShaders(shaders) { - this.state.gpuGridAggregator.updateShaders(shaders); + updateWeightParams(opts) { + const {getWeight, aggregation} = opts.props; + const {count} = this.state.weights; + count.getWeight = getWeight; + count.operation = AGGREGATION_OPERATION[aggregation]; } - _getAggregationModel() { - return this.state.gpuGridAggregator.gridAggregationModel; + updateShaders(shaders) { + if (this.state.gpuAggregation) { + this.state.gpuGridAggregator.updateShaders(shaders); + } + } + + // Private + + _alignBoundingBox(opts) { + const {screenSpaceAggregation, boundingBox, gridOffset} = this.state; + const {coordinateSystem} = opts.props; + let worldOrigin; + if (screenSpaceAggregation) { + return; + } + + switch (coordinateSystem) { + case COORDINATE_SYSTEM.LNGLAT: + case COORDINATE_SYSTEM.LNGLAT_DEPRECATED: + worldOrigin = [-180, -90]; // Origin used to define grid cell boundaries + break; + case COORDINATE_SYSTEM.IDENTITY: + const {width, height} = this.context.viewport; + worldOrigin = [-width / 2, -height / 2]; // Origin used to define grid cell boundaries + break; + default: + // Currently other coordinate systems not supported/verified. + log.assert(false); + } + + const {xMin, yMin} = boundingBox; + boundingBox.xMin = alignToCell(xMin - worldOrigin[0], gridOffset.xOffset) + worldOrigin[0]; + boundingBox.yMin = alignToCell(yMin - worldOrigin[1], gridOffset.yOffset) + worldOrigin[1]; + } + + // eslint-disable-next-line + _updateProjectionParams(opts) { + const {viewport} = this.context; + const {dataChanged, cellSizeChanged, screenSpaceAggregation} = this.state; + if (dataChanged && !screenSpaceAggregation) { + const boundingBox = getBoundingBox(this.getAttributes(), this.getNumInstances()); + this.setState({boundingBox}); + } + if (dataChanged || cellSizeChanged) { + // for grid contour layers transform cellSize from meters to lng/lat offsets + const gridOffset = this._getGridOffset(opts); + this.setState({gridOffset}); + + this._alignBoundingBox(opts); + let {width, height} = viewport; + let cellOffset = [0, 0]; + let projectPoints = false; + let translation = [0, 0]; + let scaling = [0, 0, 0]; // [x, y, z] : x,y represent scaling in x and y and z > 0 implies scaling enabled otherwise not + + if (screenSpaceAggregation) { + projectPoints = true; + scaling = [viewport.width / 2, -viewport.height / 2, 1]; + translation = [1, -1]; + } else { + const {xMin, yMin, xMax, yMax} = this.state.boundingBox; + width = xMax - xMin + gridOffset.xOffset; + height = yMax - yMin + gridOffset.yOffset; + + // Setup translations so that every point is in +ve range + cellOffset = [-1 * xMin, -1 * yMin]; + translation = [-1 * xMin, -1 * yMin]; + projectPoints = false; + } + const numCol = Math.ceil(width / gridOffset.xOffset); + const numRow = Math.ceil(height / gridOffset.yOffset); + this.allocateResources(numRow, numCol); + this.setState({ + translation, + scaling, + projectPoints, + width, + height, + cellOffset, + numCol, + numRow + }); + } + } + + _updateAggregation(opts) { + const { + cpuGridAggregator, + gpuGridAggregator, + gridOffset, + cellOffset, + translation, + scaling, + width, + height, + boundingBox, + projectPoints, + gpuAggregation, + numCol, + numRow + } = this.state; + const {props} = opts; + const {viewport} = this.context; + const attributes = this.getAttributes(); + const vertexCount = this.getNumInstances(); + + if (!gpuAggregation) { + const result = cpuGridAggregator(props, { + gridOffset, + width, + height, + projectPoints, + attributes, + viewport, + cellOffset, + boundingBox + }); + this.setState({ + layerData: result + }); + } else { + const {weights} = this.state; + gpuGridAggregator.run({ + weights, + cellSize: [gridOffset.xOffset, gridOffset.yOffset], + width, + height, + numCol, + numRow, + translation, + scaling, + vertexCount, + projectPoints, + attributes, + moduleSettings: this.getModuleSettings() + }); + } + } + + _updateWeightBins() { + const {getValue} = this.state; + + const sortedBins = new BinSorter(this.state.layerData.data || [], getValue, false); + this.setState({sortedBins}); + } + + _uploadAggregationResults() { + const {numCol, numRow} = this.state; + const {data} = this.state.layerData; + const {aggregatedBins, minValue, maxValue, totalCount} = this.state.sortedBins; + + const ELEMENTCOUNT = 4; + const aggregationSize = numCol * numRow * ELEMENTCOUNT; + const aggregationData = new Float32Array(aggregationSize).fill(0); + for (const bin of aggregatedBins) { + const {lonIdx, latIdx} = data[bin.i]; + const {value, counts} = bin; + const cellIndex = (lonIdx + latIdx * numCol) * ELEMENTCOUNT; + aggregationData[cellIndex] = value; + aggregationData[cellIndex + ELEMENTCOUNT - 1] = counts; + } + const maxMinData = new Float32Array([maxValue, 0, 0, minValue]); + const maxData = new Float32Array([maxValue, 0, 0, totalCount]); + const minData = new Float32Array([minValue, 0, 0, totalCount]); + this.updateResults({aggregationData, maxMinData, maxData, minData}); + } + + _getGridOffset(opts) { + const {cellSize, boundingBox} = this.state; + if (opts.props.coordinateSystem === COORDINATE_SYSTEM.IDENTITY) { + return {xOffset: cellSize, yOffset: cellSize}; + } + return getGridOffset(boundingBox, cellSize); + } + + _resetResults() { + const {count} = this.state.weights; + if (count) { + count.aggregationData = null; + } + } + + _updateAccessors(opts) { + if (this.state.gpuAggregation) { + this.updateWeightParams(opts); + } else { + this._updateGetValueFuncs(opts); + } + } + + _updateGetValueFuncs({oldProps, props, changeFlags}) { + const {getValue} = this.state; + if ( + !getValue || + oldProps.aggregation !== props.aggregation || + (changeFlags.updateTriggersChanged && + (changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getWeight)) + ) { + this.setState({getValue: getValueFunc(props.aggregation, props.getWeight)}); + } } } diff --git a/modules/aggregation-layers/src/grid-aggregation-layer.md b/modules/aggregation-layers/src/grid-aggregation-layer.md new file mode 100644 index 00000000000..a4a00540a65 --- /dev/null +++ b/modules/aggregation-layers/src/grid-aggregation-layer.md @@ -0,0 +1,33 @@ +# GridAggregationLayer + +This layer performs some common tasks required to perform aggregation to grid cells, especially it takes care of building required parameters for CPU and GPU aggregation and calling appropriate aggregation utility methods. Default implementation supports aggregation of single weight, subclasses can customize aggregation to multiple methods. + +This in an abstract layer, subclassed form `AggregationLayer`, `GPUGridLayer`, `ScreenGridLayer` and `ContourLayer` are subclassed from this layer. + +### Updating aggregation flags + +A layer extending this class must implement `updateAggregationFlags()` method to set following variables in `state` object : + +- `gpuAggregation` : Should be set to `true` if aggregating on `GPU`, `false` otherwise. +- `needsReProjection` : Should be set to `true` if data needs to be reprojected. For example, `ScreenGridLayer` sets this flag to true when `viewport` is changed. +- `dataChanged` : Should be set to true, if data required for aggregation is changed. +- `cellSize` : Should be set to the size of the grid cell. +- `cellSizeChanged` : Should be set to true when grid cell size is changed. + +### Customization of weights + +By default, this class aggregates single weight for each grid cell. If a subclass of this layer requires aggregating more than one weight or upload CPU aggregation results to corresponding GPU attribute buffers following private methods can be customized. + +- `updateResults`: When aggregation performed on CPU, aggregation result is in JS Array objects. Subclasses can override this method to consume aggregation data. This method is called with an object with following fields: + * `aggregationData` (*Float32Array*) - Array containing aggregation data per grid cell. Four elements per grid cell in the format `[value, 0, 0, count]`, where `value` is the aggregated weight value, up to 3 different weights. `count` is the number of objects aggregated to the grid cell. + * `maxMinData` (*Float32Array*) - Array with four values in format, `[maxValue, 0, 0, minValue]`, where `maxValue` is max of all aggregated cells. + * `maxData` (*Float32Array*) - Array with four values in format, `[maxValue, 0, 0, count]`, where `maxValue` is max of all aggregated cells and `count` is total number aggregated objects. + * `minData` (*Float32Array*) - Array with four values in format, `[minValue, 0, 0, count]`, where `minValue` is min of all aggregated cells and `count` is total number aggregated objects. + + NOTE: The goal is to match format of CPU aggregation results to that of GPU aggregation, so consumers of this data (Sublayers) don't have to change. + +- `allocateResources`: Called with following arguments to allocated resources required to hold aggregation results. + * `numRow` (*Number*) - Number of rows in the grid. + * `numCol` (*Number*) - Number of columns in the grid. + +- `updateWeightParams`: For each weight to be aggregated aggregator requires, `operation` and `getWeight` params. Subclasses can override this method to customize these parameters for each weight being aggregated. diff --git a/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.js b/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.js index e8acd9993e8..7ed17b65f1c 100644 --- a/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.js +++ b/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.js @@ -161,7 +161,9 @@ export default class HeatmapLayer extends AggregationLayer { } = this.state; const {updateTriggers, intensity, threshold} = this.props; - return new TriangleLayer( + const TriangleLayerClass = this.getSubLayerClass('triangle', TriangleLayer); + + return new TriangleLayerClass( this.getSubLayerProps({ id: 'triangle-layer', updateTriggers @@ -210,9 +212,6 @@ export default class HeatmapLayer extends AggregationLayer { // PRIVATE - _getAggregationModel() { - return this.state.weightsTransform.model; - } // override Composite layer private method to create AttributeManager instance _getAttributeManager() { return new AttributeManager(this.context.gl, { @@ -223,7 +222,7 @@ export default class HeatmapLayer extends AggregationLayer { _getChangeFlags(opts) { const changeFlags = {}; - if (this._isAggregationDirty(opts)) { + if (this.isAggregationDirty(opts)) { changeFlags.dataChanged = true; } changeFlags.viewportChanged = opts.changeFlags.viewportChanged; @@ -335,7 +334,7 @@ export default class HeatmapLayer extends AggregationLayer { } // overwrite super class method to update transform model - _updateShaders(shaderOptions) { + updateShaders(shaderOptions) { // sahder params (modules, injects) changed, update model object this._createWeightsTransform(shaderOptions); } diff --git a/modules/aggregation-layers/src/hexagon-layer/hexagon-layer.js b/modules/aggregation-layers/src/hexagon-layer/hexagon-layer.js index db70b81ebd4..43cf282d375 100644 --- a/modules/aggregation-layers/src/hexagon-layer/hexagon-layer.js +++ b/modules/aggregation-layers/src/hexagon-layer/hexagon-layer.js @@ -75,7 +75,7 @@ export default class HexagonLayer extends AggregationLayer { }; const attributeManager = this.getAttributeManager(); attributeManager.add({ - positions: {size: 3, accessor: 'getPosition' /* , type: GL.DOUBLE, fp64: false*/} + positions: {size: 3, accessor: 'getPosition'} }); // color and elevation attributes can't be added as attributes // they are calcualted using 'getValue' accessor that takes an array of pints. diff --git a/modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer.js b/modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer.js index 72938caf576..ec2c8e9ab4f 100644 --- a/modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer.js +++ b/modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer.js @@ -18,25 +18,24 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {WebMercatorViewport, log} from '@deck.gl/core'; +import {log} from '@deck.gl/core'; +import GL from '@luma.gl/constants'; import GPUGridAggregator from '../utils/gpu-grid-aggregation/gpu-grid-aggregator'; import {AGGREGATION_OPERATION} from '../utils/aggregation-operation-utils'; import ScreenGridCellLayer from './screen-grid-cell-layer'; import GridAggregationLayer from '../grid-aggregation-layer'; - -import GL from '@luma.gl/constants'; -import {Buffer} from '@luma.gl/core'; +import {getFloatTexture} from '../utils/resource-utils.js'; const defaultProps = Object.assign({}, ScreenGridCellLayer.defaultProps, { getPosition: {type: 'accessor', value: d => d.position}, - getWeight: {type: 'accessor', value: d => [1, 0, 0]}, + getWeight: {type: 'accessor', value: d => 1}, gpuAggregation: true, aggregation: 'SUM' }); // props , when changed requires re-aggregation -const AGGREGATION_PROPS = ['gpuAggregation']; +const AGGREGATION_PROPS = ['aggregation', 'getWeight']; export default class ScreenGridLayer extends GridAggregationLayer { initializeState() { @@ -47,23 +46,35 @@ export default class ScreenGridLayer extends GridAggregationLayer { log.error(`ScreenGridLayer: ${this.id} is not supported on this browser`)(); return; } - super.initializeState(AGGREGATION_PROPS); + super.initializeState({ + aggregationProps: AGGREGATION_PROPS, + getCellSize: props => props.cellSizePixels + }); const weights = { - color: { + count: { size: 1, operation: AGGREGATION_OPERATION.SUM, - needMax: true + needMax: true, + maxTexture: getFloatTexture(gl, {id: `${this.id}-max-texture`}) } }; this.setState({ supported: true, weights, - subLayerData: {attributes: {}} + subLayerData: {attributes: {}}, + screenSpaceAggregation: true, + maxTexture: weights.count.maxTexture }); const attributeManager = this.getAttributeManager(); attributeManager.add({ - positions: {size: 3, accessor: 'getPosition', type: GL.DOUBLE, fp64: false}, - color: {size: 3, accessor: 'getWeight'} + positions: { + size: 3, + accessor: 'getPosition', + type: GL.DOUBLE, + fp64: this.use64bitPositions() + }, + // this attribute is used in gpu aggregation path only + count: {size: 3, accessor: 'getWeight'} }); } @@ -73,41 +84,27 @@ export default class ScreenGridLayer extends GridAggregationLayer { updateState(opts) { super.updateState(opts); - - const cellSizeChanged = opts.props.cellSizePixels !== opts.oldProps.cellSizePixels; - const dataChanged = this._isAggregationDirty(opts); - const {viewportChanged} = opts.changeFlags; - - if (cellSizeChanged || viewportChanged) { - this._updateGridParams(); - } - - if (dataChanged || cellSizeChanged || viewportChanged) { - this._updateAggregation({ - dataChanged, - cellSizeChanged, - viewportChanged - }); - } } renderLayers() { if (!this.state.supported) { return []; } - const {maxTexture, cellCount, subLayerData} = this.state; + const {maxTexture, numRow, numCol, weights} = this.state; const {updateTriggers} = this.props; + const {aggregationBuffer} = weights.count; + const CellLayerClass = this.getSubLayerClass('cells', ScreenGridCellLayer); - return new ScreenGridCellLayer( + return new CellLayerClass( this.props, this.getSubLayerProps({ id: 'cell-layer', updateTriggers }), { - data: subLayerData, + data: {attributes: {instanceCounts: aggregationBuffer}}, maxTexture, - numInstances: cellCount + numInstances: numRow * numCol } ); } @@ -132,8 +129,8 @@ export default class ScreenGridLayer extends GridAggregationLayer { const {index} = info; if (index >= 0) { const {gpuGridAggregator} = this.state; - // Get color aggregation results - const aggregationResults = gpuGridAggregator.getData('color'); + // Get count aggregation results + const aggregationResults = gpuGridAggregator.getData('count'); // Each instance (one cell) is aggregated into single pixel, // Get current instance's aggregation details. @@ -145,90 +142,45 @@ export default class ScreenGridLayer extends GridAggregationLayer { return info; } - // Private Methods - - _getAggregationChangeFlags({oldProps, props, changeFlags}) { - const cellSizeChanged = props.cellSizePixels !== oldProps.cellSizePixels; - // props.cellMarginPixels !== oldProps.cellMarginPixels; // _TODO_ why checking margin pixels - const dataChanged = changeFlags.dataChanged || props.aggregation !== oldProps.aggregation; - const viewportChanged = changeFlags.viewportChanged; - - if (cellSizeChanged || dataChanged || viewportChanged) { - return {cellSizeChanged, dataChanged, viewportChanged}; - } + // Aggregation Overrides - return null; + updateResults({aggregationData, maxData}) { + const {count} = this.state.weights; + count.aggregationData = aggregationData; + count.aggregationBuffer.setData({data: aggregationData}); + count.maxData = maxData; + count.maxTexture.setImageData({data: maxData}); } - _updateAggregation(changeFlags) { - const vertexCount = this.getNumInstances(); - if (vertexCount <= 0) { - return; + updateAggregationFlags(opts) { + const cellSizeChanged = opts.oldProps.cellSizePixels !== opts.props.cellSizePixels; + let gpuAggregation = opts.props.gpuAggregation; + if (this.state.gpuAggregation !== opts.props.gpuAggregation) { + if (gpuAggregation && !GPUGridAggregator.isSupported(this.context.gl)) { + log.warn('GPU Grid Aggregation not supported, falling back to CPU')(); + gpuAggregation = false; + } } + const gpuAggregationChanged = gpuAggregation !== this.state.gpuAggregation; + // Consider switching between CPU and GPU aggregation as data changed as it requires + // re aggregation. + const dataChanged = + this.state.dataChanged || gpuAggregationChanged || opts.changeFlags.viewportChanged; - const {cellSizePixels, gpuAggregation} = this.props; - - const {weights} = this.state; - const {viewport} = this.context; - - weights.color.operation = - AGGREGATION_OPERATION[this.props.aggregation.toUpperCase()] || AGGREGATION_OPERATION.SUM; - - let projectPoints = false; - let gridTransformMatrix = null; - - if (this.context.viewport instanceof WebMercatorViewport) { - // project points from world space (lng/lat) to viewport (screen) space. - projectPoints = true; - } else { - projectPoints = false; - // Use pixelProjectionMatrix to transform points to viewport (screen) space. - gridTransformMatrix = viewport.pixelProjectionMatrix; - } - const results = this.state.gpuGridAggregator.run({ - weights, - cellSize: [cellSizePixels, cellSizePixels], - viewport, - changeFlags, - useGPU: gpuAggregation, - projectPoints, - gridTransformMatrix, - vertexCount, - attributes: this.getAttributes(), - moduleSettings: this.getModuleSettings() + this.setState({ + dataChanged, + cellSizeChanged, + cellSize: opts.props.cellSizePixels, + needsReProjection: dataChanged || cellSizeChanged, + gpuAggregation }); - - this.setState({maxTexture: results.color.maxTexture}); } - _updateGridParams() { - const {width, height} = this.context.viewport; - const {cellSizePixels} = this.props; - const {gl} = this.context; + // Private - const numCol = Math.ceil(width / cellSizePixels); - const numRow = Math.ceil(height / cellSizePixels); - const cellCount = numCol * numRow; - const dataBytes = cellCount * 4 * 4; - let aggregationBuffer = this.state.aggregationBuffer; - if (aggregationBuffer) { - aggregationBuffer.delete(); - } - - aggregationBuffer = new Buffer(gl, { - byteLength: dataBytes, - accessor: { - size: 4, - type: GL.FLOAT, - divisor: 1 - } - }); - this.state.weights.color.aggregationBuffer = aggregationBuffer; - this.state.subLayerData.attributes.instanceCounts = aggregationBuffer; - this.setState({ - cellCount, - aggregationBuffer - }); + _getGridOffset() { + const {cellSize} = this.state; + return {xOffset: cellSize, yOffset: cellSize}; } } diff --git a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-all-vs-64.glsl.js b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-all-vs.glsl.js similarity index 100% rename from modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-all-vs-64.glsl.js rename to modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-all-vs.glsl.js diff --git a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-to-grid-vs-64.glsl.js b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-to-grid-vs-64.glsl.js deleted file mode 100644 index 1556912588e..00000000000 --- a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-to-grid-vs-64.glsl.js +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2015 - 2018 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -export default `\ -#define SHADER_NAME gpu-aggregation-to-grid-vs-64 - -attribute vec2 positions; -attribute vec2 positions64Low; -attribute vec3 weights; -uniform vec2 windowSize; -uniform vec2 cellSize; -uniform vec2 gridSize; -uniform vec2 uProjectionMatrixFP64[16]; -uniform bool projectPoints; - -varying vec3 vWeights; - -void project_to_pixel(vec2 pos, vec2 pos64Low, out vec2 pixelXY64[2]) { - - vec2 result64[4]; - vec2 position64[4]; - position64[0] = vec2(pos.x, pos64Low.x); - position64[1] = vec2(pos.y, pos64Low.y); - position64[2] = vec2(0., 0.); - position64[3] = vec2(1., 0.); - mat4_vec4_mul_fp64(uProjectionMatrixFP64, position64, - result64); - - pixelXY64[0] = div_fp64(result64[0], result64[3]); - pixelXY64[1] = div_fp64(result64[1], result64[3]); -} - -void main(void) { - - vWeights = weights; - - vec2 windowPos = positions; - vec2 windowPos64Low = positions64Low; - if (projectPoints) { - vec2 projectedXY[2]; - project_position_fp64(windowPos, windowPos64Low, projectedXY); - windowPos.x = projectedXY[0].x; - windowPos.y = projectedXY[1].x; - windowPos64Low.x = projectedXY[0].y; - windowPos64Low.y = projectedXY[1].y; - } - - vec2 pixelXY64[2]; - project_to_pixel(windowPos, windowPos64Low, pixelXY64); - - // Transform (0,0):windowSize -> (0, 0): gridSize - vec2 gridXY64[2]; - gridXY64[0] = div_fp64(pixelXY64[0], vec2(cellSize.x, 0)); - gridXY64[1] = div_fp64(pixelXY64[1], vec2(cellSize.y, 0)); - float x = floor(gridXY64[0].x); - float y = floor(gridXY64[1].x); - vec2 pos = vec2(x, y); - - // Transform (0,0):gridSize -> (-1, -1):(1,1) - pos = (pos * (2., 2.) / (gridSize)) - (1., 1.); - - // Move to pixel center, pixel-size in screen sapce (2/gridSize) * 0.5 => 1/gridSize - vec2 offset = 1.0 / gridSize; - pos = pos + offset; - - gl_Position = vec4(pos, 0.0, 1.0); -} -`; diff --git a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-to-grid-vs.glsl.js b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-to-grid-vs.glsl.js index 08ecc4383bd..e4124fef977 100644 --- a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-to-grid-vs.glsl.js +++ b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/aggregate-to-grid-vs.glsl.js @@ -21,34 +21,48 @@ export default `\ #define SHADER_NAME gpu-aggregation-to-grid-vs -attribute vec2 positions; +attribute vec3 positions; +attribute vec3 positions64Low; attribute vec3 weights; uniform vec2 windowSize; uniform vec2 cellSize; uniform vec2 gridSize; -uniform mat4 uProjectionMatrix; uniform bool projectPoints; +uniform vec2 translation; +uniform vec3 scaling; varying vec3 vWeights; vec2 project_to_pixel(vec4 pos) { - vec4 result = uProjectionMatrix * pos; - return result.xy/result.w; + vec4 result; + pos.xy = pos.xy/pos.w; + result = pos + vec4(translation, 0., 0.); + result.xy = scaling.z > 0. ? result.xy * scaling.xy : result.xy; + return result.xy; } void main(void) { vWeights = weights; - vec4 windowPos = vec4(positions, 0, 1.); + vec4 windowPos = vec4(positions, 1.); if (projectPoints) { - windowPos = project_position_to_clipspace(vec3(positions, 0), vec3(0), vec3(0)); + windowPos = project_position_to_clipspace(positions, positions64Low, vec3(0)); } vec2 pos = project_to_pixel(windowPos); + vec2 pixelXY64[2]; + pixelXY64[0] = vec2(pos.x, 0.); + pixelXY64[1] = vec2(pos.y, 0.); + // Transform (0,0):windowSize -> (0, 0): gridSize - pos = floor(pos / cellSize); + vec2 gridXY64[2]; + gridXY64[0] = div_fp64(pixelXY64[0], vec2(cellSize.x, 0)); + gridXY64[1] = div_fp64(pixelXY64[1], vec2(cellSize.y, 0)); + float x = floor(gridXY64[0].x); + float y = floor(gridXY64[1].x); + pos = vec2(x, y); // Transform (0,0):gridSize -> (-1, -1):(1,1) pos = (pos * (2., 2.) / (gridSize)) - (1., 1.); @@ -57,6 +71,7 @@ void main(void) { vec2 offset = 1.0 / gridSize; pos = pos + offset; + gl_Position = vec4(pos, 0.0, 1.0); } `; diff --git a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator-constants.js b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator-constants.js index 5edc56f7be8..726d20caade 100644 --- a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator-constants.js +++ b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator-constants.js @@ -9,10 +9,7 @@ export const DEFAULT_CHANGE_FLAGS = { export const DEFAULT_RUN_PARAMS = { changeFlags: DEFAULT_CHANGE_FLAGS, projectPoints: false, - useGPU: true, - fp64: false, viewport: null, - gridTransformMatrix: null, createBufferObjects: true, moduleSettings: {} }; @@ -37,7 +34,6 @@ export const DEFAULT_WEIGHT_PARAMS = { combineMaxMin: false }; -export const IDENTITY_MATRIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; export const PIXEL_SIZE = 4; // RGBA32F export const WEIGHT_SIZE = 3; diff --git a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator.js b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator.js index e3d5241bc15..832aaab3cab 100644 --- a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator.js +++ b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator.js @@ -20,43 +20,35 @@ import GL from '@luma.gl/constants'; import { - Buffer, Model, Transform, FEATURES, hasFeatures, isWebGL2, readPixelsToBuffer, - fp64 as fp64ShaderModule, + fp64, withParameters } from '@luma.gl/core'; -import {log, project32, project64, mergeShaders} from '@deck.gl/core'; -import {worldToPixels} from '@math.gl/web-mercator'; -const {fp64ifyMatrix4} = fp64ShaderModule; +import {log, project32, mergeShaders} from '@deck.gl/core'; import { - DEFAULT_CHANGE_FLAGS, DEFAULT_RUN_PARAMS, MAX_32_BIT_FLOAT, MIN_BLEND_EQUATION, MAX_BLEND_EQUATION, MAX_MIN_BLEND_EQUATION, EQUATION_MAP, - ELEMENTCOUNT, DEFAULT_WEIGHT_PARAMS, - IDENTITY_MATRIX, - PIXEL_SIZE, - WEIGHT_SIZE + PIXEL_SIZE } from './gpu-grid-aggregator-constants'; import {AGGREGATION_OPERATION} from '../aggregation-operation-utils'; import AGGREGATE_TO_GRID_VS from './aggregate-to-grid-vs.glsl'; -import AGGREGATE_TO_GRID_VS_FP64 from './aggregate-to-grid-vs-64.glsl'; import AGGREGATE_TO_GRID_FS from './aggregate-to-grid-fs.glsl'; -import AGGREGATE_ALL_VS_FP64 from './aggregate-all-vs-64.glsl'; +import AGGREGATE_ALL_VS from './aggregate-all-vs.glsl'; import AGGREGATE_ALL_FS from './aggregate-all-fs.glsl'; import TRANSFORM_MEAN_VS from './transform-mean-vs.glsl'; -import {getFloatTexture, getFramebuffer, getFloatArray} from './../resource-utils.js'; +import {getFloatTexture, getFramebuffer} from './../resource-utils.js'; const BUFFER_NAMES = ['aggregationBuffer', 'maxMinBuffer', 'minBuffer', 'maxBuffer']; const ARRAY_BUFFER_MAP = { @@ -147,19 +139,6 @@ export default class GPUGridAggregator { this.id = opts.id || 'gpu-grid-aggregator'; this.gl = gl; this.state = { - // cache weights and position data to process when data is not changed - weights: null, - gridPositions: null, - vertexCount: 0, - - // flags/variables that affect the aggregation - fp64: null, - useGPU: null, - numCol: 0, - numRow: 0, - windowSize: null, - cellSize: null, - // per weight GPU resources weightAttributes: {}, textures: {}, @@ -186,7 +165,7 @@ export default class GPUGridAggregator { FEATURES.TEXTURE_FLOAT // sample from a float texture ); if (this._hasGPUSupport) { - this.setupModels(); + this._setupModels(); } } @@ -223,16 +202,11 @@ export default class GPUGridAggregator { run(opts = {}) { // reset results this.setState({results: {}}); - const aggregationParams = this.getAggregationParams(opts); - this.updateGridSize(aggregationParams); - const {useGPU} = aggregationParams; - if (this._hasGPUSupport && useGPU) { - return this.runAggregationOnGPU(aggregationParams); - } - if (useGPU) { - log.warn('GPUGridAggregator: GPU Aggregation not supported, falling back to CPU')(); + const aggregationParams = this._normalizeAggregationParams(opts); + if (!this._hasGPUSupport) { + log.log(1, 'GPUGridAggregator: not supported')(); } - return this.runAggregationOnCPU(aggregationParams); + return this._runAggregation(aggregationParams); } // Reads aggregation data into JS Array object @@ -261,52 +235,29 @@ export default class GPUGridAggregator { return data; } + updateShaders(shaderOptions = {}) { + this.setState({shaderOptions, modelDirty: true}); + } + // PRIVATE - getAggregationParams(opts) { + _normalizeAggregationParams(opts) { const aggregationParams = Object.assign({}, DEFAULT_RUN_PARAMS, opts); - const { - useGPU, - gridTransformMatrix, - moduleSettings, - weights, - projectPoints, - cellSize - } = aggregationParams; + const {moduleSettings, weights, cellSize, numCol, numRow} = aggregationParams; const {viewport} = moduleSettings; - if (this.state.useGPU !== useGPU) { - // CPU/GPU resources need to reinitialized, force set the change flags. - aggregationParams.changeFlags = Object.assign( - {}, - aggregationParams.changeFlags, - DEFAULT_CHANGE_FLAGS - ); - } - if ( - cellSize && - (!this.state.cellSize || - this.state.cellSize[0] !== cellSize[0] || - this.state.cellSize[1] !== cellSize[1]) - ) { - aggregationParams.changeFlags.cellSizeChanged = true; - // For GridLayer aggregation, cellSize is calculated by parsing all input data as it depends - // on bounding box, cache cellSize - this.setState({cellSize}); - } - // validateProps(aggregationParams, opts); - this.setState({useGPU}); - aggregationParams.gridTransformMatrix = - (projectPoints ? viewport.viewportMatrix : gridTransformMatrix) || IDENTITY_MATRIX; - if (weights) { aggregationParams.weights = normalizeWeightParams(weights); + } - // cache weights to process when only cellSize or viewport is changed. - // position data is cached in Buffers for GPU case and in 'gridPositions' for CPU case. - this.setState({weights: aggregationParams.weights}); + if (!numCol || !numRow) { + const width = opts.width || viewport.width; + const height = opts.height || viewport.height; + aggregationParams.numCol = numCol || Math.ceil(width / cellSize[0]); + aggregationParams.numRow = numRow || Math.ceil(height / cellSize[1]); } + return aggregationParams; } @@ -315,305 +266,9 @@ export default class GPUGridAggregator { Object.assign(this.state, updateObject); } - updateGridSize(opts) { - const {moduleSettings, cellSize} = opts; - const {viewport} = moduleSettings; - const width = opts.width || viewport.width; - const height = opts.height || viewport.height; - const numCol = Math.ceil(width / cellSize[0]); - const numRow = Math.ceil(height / cellSize[1]); - this.setState({numCol, numRow, windowSize: [width, height]}); - } - - // CPU Aggregation methods - - // aggregated weight value to a cell - /* eslint-disable max-depth */ - calculateAggregationData(opts) { - const {weights, results, cellIndex, posIndex, attributes} = opts; - for (const id in weights) { - const {size, operation} = weights[id]; - // TODO - value might not exist (e.g. attribute transition) - const values = attributes[id].value; - const {aggregationData} = results[id]; - for (let sizeIndex = 0; sizeIndex < size; sizeIndex++) { - const cellElementIndex = cellIndex + sizeIndex; - const weightComponent = values[posIndex * WEIGHT_SIZE + sizeIndex]; - - if (aggregationData[cellIndex + 3] === 0) { - // if the cell is getting update the first time, set the value directly. - aggregationData[cellElementIndex] = weightComponent; - } else { - switch (operation) { - case AGGREGATION_OPERATION.SUM: - case AGGREGATION_OPERATION.MEAN: - aggregationData[cellElementIndex] += weightComponent; - // MEAN value is calculated during 'calculateMeanMaxMinData' - break; - case AGGREGATION_OPERATION.MIN: - aggregationData[cellElementIndex] = Math.min( - aggregationData[cellElementIndex], - weightComponent - ); - break; - case AGGREGATION_OPERATION.MAX: - aggregationData[cellElementIndex] = Math.max( - aggregationData[cellElementIndex], - weightComponent - ); - break; - default: - // Not a valid operation enum. - log.assert(false); - break; - } - } - } - - // Track the count per grid-cell - aggregationData[cellIndex + 3]++; - } - } - - /* eslint-disable max-depth, complexity */ - calculateMeanMaxMinData(opts) { - const {validCellIndices, results, weights} = opts; - - // collect max/min values - validCellIndices.forEach(cellIndex => { - for (const id in results) { - const {size, needMin, needMax, operation} = weights[id]; - const {aggregationData, minData, maxData, maxMinData} = results[id]; - const calculateMinMax = needMin || needMax; - const calculateMean = operation === AGGREGATION_OPERATION.MEAN; - const combineMaxMin = needMin && needMax && weights[id].combineMaxMin; - const count = aggregationData[cellIndex + ELEMENTCOUNT - 1]; - for ( - let sizeIndex = 0; - sizeIndex < size && (calculateMinMax || calculateMean); - sizeIndex++ - ) { - const cellElementIndex = cellIndex + sizeIndex; - let weight = aggregationData[cellElementIndex]; - if (calculateMean) { - aggregationData[cellElementIndex] /= count; - weight = aggregationData[cellElementIndex]; - } - if (combineMaxMin) { - // use RGB for max values for 3 weights. - maxMinData[sizeIndex] = Math.max(maxMinData[sizeIndex], weight); - } else { - if (needMin) { - minData[sizeIndex] = Math.min(minData[sizeIndex], weight); - } - if (needMax) { - maxData[sizeIndex] = Math.max(maxData[sizeIndex], weight); - } - } - } - // update total aggregation values. - if (combineMaxMin) { - // Use Alpha channel to store total min value for weight#0 - maxMinData[ELEMENTCOUNT - 1] = Math.min( - maxMinData[ELEMENTCOUNT - 1], - aggregationData[cellIndex + 0] - ); - } else { - // Use Alpha channel to store total counts. - if (needMin) { - minData[ELEMENTCOUNT - 1] += count; - } - if (needMax) { - maxData[ELEMENTCOUNT - 1] += count; - } - } - } - }); - } - /* eslint-enable max-depth */ - - initCPUResults(opts) { - const weights = opts.weights || this.state.weights; - const {numCol, numRow} = this.state; - const results = {}; - // setup results object - for (const id in weights) { - let {aggregationData, minData, maxData, maxMinData} = weights[id]; - const {needMin, needMax} = weights[id]; - const combineMaxMin = needMin && needMax && weights[id].combineMaxMin; - - const aggregationSize = numCol * numRow * ELEMENTCOUNT; - aggregationData = getFloatArray(aggregationData, aggregationSize); - if (combineMaxMin) { - maxMinData = getFloatArray(maxMinData, ELEMENTCOUNT); - // RGB for max value - maxMinData.fill(-Infinity, 0, ELEMENTCOUNT - 1); - // Alpha for min value - maxMinData[ELEMENTCOUNT - 1] = Infinity; - } else { - // RGB for min/max values - // Alpha for total count - if (needMin) { - minData = getFloatArray(minData, ELEMENTCOUNT, Infinity); - minData[ELEMENTCOUNT - 1] = 0; - } - if (needMax) { - maxData = getFloatArray(maxData, ELEMENTCOUNT, -Infinity); - maxData[ELEMENTCOUNT - 1] = 0; - } - } - results[id] = Object.assign({}, weights[id], { - aggregationData, - minData, - maxData, - maxMinData - }); - } - return results; - } - - /* eslint-disable max-statements */ - runAggregationOnCPU(opts) { - const { - attributes, - vertexCount, - cellSize, - gridTransformMatrix, - moduleSettings, - projectPoints - } = opts; - let {weights} = opts; - const {numCol, numRow} = this.state; - let {gridPositions} = this.state; - const {viewport} = moduleSettings; - const results = this.initCPUResults(opts); - // screen space or world space projection required - const gridTransformRequired = !gridPositions || shouldTransformToGrid(opts); - const pos = [0, 0, 0]; - - log.assert(gridTransformRequired || opts.changeFlags.cellSizeChanged); - - if (gridTransformRequired) { - gridPositions = new Float64Array(vertexCount * 2); - this.setState({gridPositions}); - } else { - gridPositions = this.state.gridPositions; - weights = this.state.weights; - } - - const validCellIndices = new Set(); - const positions = attributes.positions.value; - const posSize = 3; - for (let posIndex = 0; posIndex < vertexCount; posIndex++) { - let x; - let y; - if (gridTransformRequired) { - pos[0] = positions[posIndex * posSize]; - pos[1] = positions[posIndex * posSize + 1]; - if (projectPoints) { - [x, y] = viewport.project(pos); - } else { - [x, y] = worldToPixels(pos, gridTransformMatrix); - } - gridPositions[posIndex * 2] = x; - gridPositions[posIndex * 2 + 1] = y; - } else { - x = gridPositions[posIndex * 2]; - y = gridPositions[posIndex * 2 + 1]; - } - - const colId = Math.floor(x / cellSize[0]); - const rowId = Math.floor(y / cellSize[1]); - if (colId >= 0 && colId < numCol && rowId >= 0 && rowId < numRow) { - const cellIndex = (colId + rowId * numCol) * ELEMENTCOUNT; - validCellIndices.add(cellIndex); - this.calculateAggregationData({weights, results, cellIndex, posIndex, attributes}); - } - } - - this.calculateMeanMaxMinData({validCellIndices, results, weights}); - - // Update buffer objects. - this.updateAggregationBuffers(opts, results); - - this.setState({results}); - return results; - } - /* eslint-disable max-statements */ - - _uploadResultsToGPU({gl, bufferName, textureName, id, data, result}) { - const {resources} = this.state; - const resourceName = `cpu-result-${id}-${bufferName}`; - result[bufferName] = result[bufferName] || resources[resourceName]; - if (result[bufferName]) { - result[bufferName].setData({data}); - } else { - // save resource for garbage collection - resources[resourceName] = new Buffer(gl, data); - result[bufferName] = resources[resourceName]; - } - - // Upload result to a texture - if (textureName) { - const texture = this._getMinMaxTexture(`${id}-textureName`); - texture.setImageData({data}); - result[textureName] = texture; - } - } - - updateAggregationBuffers(opts, results) { - if (!opts.createBufferObjects) { - return; - } - const weights = opts.weights || this.state.weights; - for (const id in results) { - const {aggregationData, minData, maxData, maxMinData} = results[id]; - const {needMin, needMax} = weights[id]; - const combineMaxMin = needMin && needMax && weights[id].combineMaxMin; - this._uploadResultsToGPU({ - gl: this.gl, - bufferName: 'aggregationBuffer', - id, - data: aggregationData, - result: results[id] - }); - if (combineMaxMin) { - this._uploadResultsToGPU({ - gl: this.gl, - bufferName: 'maxMinBuffer', - textureName: 'maxMinTexture', - id, - data: maxMinData, - result: results[id] - }); - } else { - if (needMin) { - this._uploadResultsToGPU({ - gl: this.gl, - bufferName: 'minBuffer', - textureName: 'minTexture', - id, - data: minData, - result: results[id] - }); - } - if (needMax) { - this._uploadResultsToGPU({ - gl: this.gl, - bufferName: 'maxBuffer', - textureName: 'maxTexture', - id, - data: maxData, - result: results[id] - }); - } - } - } - } - // GPU Aggregation methods - getAggregateData(opts) { + _getAggregateData(opts) { const results = {}; const { textures, @@ -621,9 +276,9 @@ export default class GPUGridAggregator { maxMinFramebuffers, minFramebuffers, maxFramebuffers, - weights, resources } = this.state; + const {weights} = opts; for (const id in weights) { results[id] = {}; @@ -656,23 +311,26 @@ export default class GPUGridAggregator { } } } - this.trackGPUResultBuffers(results, weights); + this._trackGPUResultBuffers(results, weights); return results; } - renderAggregateData(opts) { - const {cellSize, gridTransformMatrix, projectPoints, attributes, moduleSettings} = opts; + _renderAggregateData(opts) { const { + cellSize, + projectPoints, + attributes, + moduleSettings, numCol, numRow, - windowSize, - maxMinFramebuffers, - minFramebuffers, - maxFramebuffers, - weights - } = this.state; + width, + height, + weights, + translation, + scaling + } = opts; + const {maxMinFramebuffers, minFramebuffers, maxFramebuffers} = this.state; - const uProjectionMatrixFP64 = fp64ifyMatrix4(gridTransformMatrix); const gridSize = [numCol, numRow]; const parameters = { blend: true, @@ -680,20 +338,28 @@ export default class GPUGridAggregator { blendFunc: [GL.ONE, GL.ONE] }; const uniforms = { - windowSize, + windowSize: [width, height], cellSize, gridSize, - uProjectionMatrix: gridTransformMatrix, - uProjectionMatrixFP64, - projectPoints + projectPoints, + translation, + scaling }; for (const id in weights) { const {needMin, needMax} = weights[id]; const combineMaxMin = needMin && needMax && weights[id].combineMaxMin; - this.renderToWeightsTexture({id, parameters, moduleSettings, uniforms, gridSize, attributes}); + this._renderToWeightsTexture({ + id, + parameters, + moduleSettings, + uniforms, + gridSize, + attributes, + weights + }); if (combineMaxMin) { - this.renderToMaxMinTexture({ + this._renderToMaxMinTexture({ id, parameters: Object.assign({}, parameters, {blendEquation: MAX_MIN_BLEND_EQUATION}), gridSize, @@ -703,7 +369,7 @@ export default class GPUGridAggregator { }); } else { if (needMin) { - this.renderToMaxMinTexture({ + this._renderToMaxMinTexture({ id, parameters: Object.assign({}, parameters, {blendEquation: MIN_BLEND_EQUATION}), gridSize, @@ -713,11 +379,12 @@ export default class GPUGridAggregator { }); } if (needMax) { - this.renderToMaxMinTexture({ + this._renderToMaxMinTexture({ id, parameters: Object.assign({}, parameters, {blendEquation: MAX_BLEND_EQUATION}), gridSize, minOrMaxFb: maxFramebuffers[id], + clearParams: {clearColor: [0, 0, 0, 0]}, combineMaxMin }); } @@ -726,7 +393,7 @@ export default class GPUGridAggregator { } // render all aggregated grid-cells to generate Min, Max or MaxMin data texture - renderToMaxMinTexture(opts) { + _renderToMaxMinTexture(opts) { const {id, parameters, gridSize, minOrMaxFb, combineMaxMin, clearParams = {}} = opts; const {framebuffers} = this.state; const {gl, allAggregationModel} = this; @@ -748,9 +415,9 @@ export default class GPUGridAggregator { } // render all data points to aggregate weights - renderToWeightsTexture(opts) { - const {id, parameters, moduleSettings, uniforms, gridSize} = opts; - const {framebuffers, equations, weightAttributes, weights} = this.state; + _renderToWeightsTexture(opts) { + const {id, parameters, moduleSettings, uniforms, gridSize, weights} = opts; + const {framebuffers, equations, weightAttributes} = this.state; const {gl, gridAggregationModel} = this; const {operation} = weights[id]; @@ -797,30 +464,29 @@ export default class GPUGridAggregator { } } - runAggregationOnGPU(opts) { - this.updateModels(opts); - this.setupFramebuffers(opts); - this.renderAggregateData(opts); - const results = this.getAggregateData(opts); + _runAggregation(opts) { + this._updateModels(opts); + this._setupFramebuffers(opts); + this._renderAggregateData(opts); + const results = this._getAggregateData(opts); this.setState({results}); return results; } // set up framebuffer for each weight - /* eslint-disable complexity, max-depth */ - setupFramebuffers(opts) { + /* eslint-disable complexity, max-depth, max-statements*/ + _setupFramebuffers(opts) { const { - numCol, - numRow, textures, framebuffers, maxMinFramebuffers, minFramebuffers, maxFramebuffers, meanTextures, - equations, - weights + equations } = this.state; + const {weights} = opts; + const {numCol, numRow} = opts; const framebufferSize = {width: numCol, height: numRow}; for (const id in weights) { const {needMin, needMax, combineMaxMin, operation} = weights[id]; @@ -854,13 +520,13 @@ export default class GPUGridAggregator { if (needMin || needMax) { if (needMin && needMax && combineMaxMin) { if (!maxMinFramebuffers[id]) { - texture = this._getMinMaxTexture(`${id}-maxMinTexture`); + texture = weights[id].maxMinTexture || this._getMinMaxTexture(`${id}-maxMinTexture`); maxMinFramebuffers[id] = getFramebuffer(this.gl, {id: `${id}-maxMinFb`, texture}); } } else { if (needMin) { if (!minFramebuffers[id]) { - texture = this._getMinMaxTexture(`${id}-minTexture`); + texture = weights[id].minTexture || this._getMinMaxTexture(`${id}-minTexture`); minFramebuffers[id] = getFramebuffer(this.gl, { id: `${id}-minFb`, texture @@ -869,7 +535,7 @@ export default class GPUGridAggregator { } if (needMax) { if (!maxFramebuffers[id]) { - texture = this._getMinMaxTexture(`${id}-maxTexture`); + texture = weights[id].maxTexture || this._getMinMaxTexture(`${id}-maxTexture`); maxFramebuffers[id] = getFramebuffer(this.gl, { id: `${id}-maxFb`, texture @@ -880,7 +546,7 @@ export default class GPUGridAggregator { } } } - /* eslint-enable complexity, max-depth */ + /* eslint-enable complexity, max-depth, max-statements */ _getMinMaxTexture(name) { const {resources} = this.state; @@ -890,29 +556,23 @@ export default class GPUGridAggregator { return resources[name]; } - setupModels(fp64 = false) { - this.setupAggregationModel(fp64); - if (!this.allAggregationModel) { - const {gl} = this; - const {numCol, numRow} = this.state; - const instanceCount = numCol * numRow; - // Model doesn't have to change when fp64 flag changes - this.allAggregationModel = getAllAggregationModel(gl, instanceCount); - } - } - - setupAggregationModel(fp64 = false) { + _setupModels({numCol = 0, numRow = 0} = {}) { const {gl} = this; const {shaderOptions} = this.state; if (this.gridAggregationModel) { this.gridAggregationModel.delete(); } - this.gridAggregationModel = getAggregationModel(gl, shaderOptions, fp64); + this.gridAggregationModel = getAggregationModel(gl, shaderOptions); + if (!this.allAggregationModel) { + const instanceCount = numCol * numRow; + this.allAggregationModel = getAllAggregationModel(gl, instanceCount); + } } // set up buffers for all weights - setupWeightAttributes(opts) { - const {weightAttributes, weights} = this.state; + _setupWeightAttributes(opts) { + const {weightAttributes} = this.state; + const {weights} = opts; for (const id in weights) { weightAttributes[id] = opts.attributes[id]; } @@ -920,7 +580,7 @@ export default class GPUGridAggregator { // GPU Aggregation results are provided in Buffers, if new Buffer objects are created track them for later deletion. /* eslint-disable max-depth */ - trackGPUResultBuffers(results, weights) { + _trackGPUResultBuffers(results, weights) { const {resources} = this.state; for (const id in results) { if (results[id]) { @@ -940,30 +600,22 @@ export default class GPUGridAggregator { } /* eslint-enable max-depth */ - /* eslint-disable max-statements */ - updateModels(opts) { - const {changeFlags, vertexCount, attributes} = opts; - const {numCol, numRow, modelDirty} = this.state; + _updateModels(opts) { + const {vertexCount, attributes, numCol, numRow} = opts; + const {modelDirty} = this.state; - if (opts.fp64 !== this.state.fp64 || modelDirty) { - this.setupModels(opts.fp64); - this.setState({fp64: opts.fp64, modelDirty: false}); + if (modelDirty) { + this._setupModels(opts); + this.setState({modelDirty: false}); } // this maps color/elevation to weight name. - this.setupWeightAttributes(opts); + this._setupWeightAttributes(opts); this.gridAggregationModel.setVertexCount(vertexCount); this.gridAggregationModel.setAttributes(attributes); - if (changeFlags.cellSizeChanged || changeFlags.viewportChanged) { - this.allAggregationModel.setInstanceCount(numCol * numRow); - } - } - /* eslint-enable max-statements */ - - updateShaders(shaderOptions = {}) { - this.setState({shaderOptions, modelDirty: true}); + this.allAggregationModel.setInstanceCount(numCol * numRow); } } @@ -986,30 +638,19 @@ function deleteResources(resources) { }); } -function shouldTransformToGrid(opts) { - const {projectPoints, changeFlags} = opts; - if ( - changeFlags.dataChanged || - (projectPoints && changeFlags.viewportChanged) // world space aggregation (GridLayer) doesn't change when viewport is changed. - ) { - return true; - } - return false; -} - -function getAggregationModel(gl, shaderOptions, fp64 = false) { +function getAggregationModel(gl, shaderOptions) { const shaders = mergeShaders( { - vs: fp64 ? AGGREGATE_TO_GRID_VS_FP64 : AGGREGATE_TO_GRID_VS, + vs: AGGREGATE_TO_GRID_VS, fs: AGGREGATE_TO_GRID_FS, - modules: fp64 ? [project64] : [project32] + modules: [fp64, project32] }, shaderOptions ); return new Model(gl, { id: 'Gird-Aggregation-Model', - vertexCount: 0, + vertexCount: 1, drawMode: GL.POINTS, ...shaders }); @@ -1018,9 +659,9 @@ function getAggregationModel(gl, shaderOptions, fp64 = false) { function getAllAggregationModel(gl, instanceCount) { return new Model(gl, { id: 'All-Aggregation-Model', - vs: AGGREGATE_ALL_VS_FP64, + vs: AGGREGATE_ALL_VS, fs: AGGREGATE_ALL_FS, - modules: [fp64ShaderModule], + modules: [fp64], vertexCount: 1, drawMode: GL.POINTS, isInstanced: true, @@ -1044,30 +685,3 @@ function getMeanTransform(gl, opts) { ) ); } - -/* eslint-disable complexity */ -// DEBUG ONLY -// validateProps(aggregationParams, opts) { -// const {changeFlags, projectPoints, gridTransformMatrix} = aggregationParams; -// log.assert( -// changeFlags.dataChanged || changeFlags.viewportChanged || changeFlags.cellSizeChanged -// ); -// -// // log.assert for required options -// log.assert( -// !changeFlags.dataChanged || -// (opts.attributes && -// opts.weights && -// (!opts.projectPositions || opts.moduleSettings.viewport) && -// opts.cellSize) -// ); -// log.assert(!changeFlags.cellSizeChanged || opts.cellSize); -// -// // viewport is needed only when performing screen space aggregation (projectPoints is true) -// log.assert(!(changeFlags.viewportChanged && projectPoints) || opts.moduleSettings.viewport); -// -// if (projectPoints && gridTransformMatrix) { -// log.warn('projectPoints is true, gridTransformMatrix is ignored')(); -// } -// } -/* eslint-enable complexity */ diff --git a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator.md b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator.md index 4d1e852df70..bd4dfd4f829 100644 --- a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator.md +++ b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/gpu-grid-aggregator.md @@ -1,21 +1,19 @@ -# GPUGridAggregator Class (Advanced) +# GPUGridAggregator Class (Advanced) (WebGL2) -`GPUGridAggregator` performs grid aggregation on GPU under WebGL2, falls back to CPU aggregation otherwise. Aggregation can be performed either in world space or in screen space. +`GPUGridAggregator` performs grid aggregation on GPU. Aggregation can be performed either in world space or in screen space. # Usage (Aggregation in World space) ``` const aggregator = new GPUGridAggregator(gl); const results = aggregator.run({ - positions: [-120.4193, 34.7751, -118.67079, 34.03948, ... ] // lng, lat pairs + attributes, // position and weight attributes weights: { weight1: { - values: [ 2, 10, ... ] // weight per point operation: AGGREGATION_OPERATION.MEAN, needMax: true, } }, - useGPU: true, cellSize: [50, 50], width: 500, height: 500 @@ -34,15 +32,13 @@ You can also perform aggregation in screen space by provide a viewport and set ` const aggregator = new GPUGridAggregator(gl); const results = aggregator.run({ - positions: [-120.4193, 34.7751, -118.67079, 34.03948, ... ] // lng, lat pairs + attributes, // position and weight attributes weights: { weight1: { - values: [ 2, 10, ... ] // weight per point operation: AGGREGATION_OPERATION.MEAN, needMax: true, } }, - useGPU: true, cellSize: [50, 50], width: 500, height: 500, @@ -76,15 +72,14 @@ const results = gpuGridAggregator.run({ cellSize: [5, 5], viewport, changeFlags, - useGPU: true, projectPoints: true, - gridTransformMatrix + translation, + scaling }); ``` Parameters: * positions (Array) : Array of points in world space (lng, lat). -* positions64xyLow (Array, Optional) : Array of low precision values of points in world space (lng, lat). * weights (Object) : Object contains one or more weights. Key represents id and corresponding object represents the weight. Each weight object contains following values: * `values` (Array, Float32Array or Buffer) : Contains weight values for all points, there should be 3 floats for each point, un-used values can be 0. @@ -92,13 +87,12 @@ Parameters: * `operation` (Enum {SUM, MEAN, MIN or MAX}, default: SUM) : Defines aggregation operation. * `needMin` (Boolean, default: false) : when true additional aggregation steps are performed to calculate minimum of all aggregation values and total count, result object will contain minBuffer. * `needMax` (Boolean, default: false) : when true additional aggregation steps are performed to calculate maximum of all aggregation values and total count, result object will contain maxBuffer. - * `combineMaxMin` (Boolean, default: false) : Applicable only when `needMin` and `needMax` are set. When true, both min and max values are calculated in single aggregation step using `blendEquationSeparate` WebGL API. But since Alpha channel can only contain one float, it will only provide minimum value for first weight in Alpha channel and RGB channels store maximum value upto 3 weights. Also when selected total count is not availabe. Result object will contain maxMinBuffer. + * `combineMaxMin` (Boolean, default: false) : Applicable only when `needMin` and `needMax` are set. When true, both min and max values are calculated in single aggregation step using `blendEquationSeparate` WebGL API. But since Alpha channel can only contain one float, it will only provide minimum value for first weight in Alpha channel and RGB channels store maximum value up to 3 weights. Also when selected total count is not available. Result object will contain maxMinBuffer. * cellSize: (Array) : Size of the cell, cellSize[0] is width and cellSize[1] is height. * width: (Number, Optional) : Grid width in pixels, deduced from ‘viewport’ when not provided. * height: (Number, Optional) : Grid height in pixels, deduced from ‘viewport’ when not provided. * viewport: (Object, Viewport) : Contains size of viewport and also used to perform projection. -* useGPU: (Boolean, optional, default: true) : When true and browser supports required WebGL features, aggregation is performed on GPU, otherwise on CPU. * changeFlags: (Object, Optional) : Object with following keyed values, that determine whether to re-create internal WebGL resources for performing aggregation compared to last run. If no value is provided, all flags are treated to be true. * dataChanged (Bool) : should be set to true when data is changed. * viewportChanged (Bool) : should be set to true when viewport is changed. @@ -106,9 +100,12 @@ Parameters: * countsBuffer: (Buffer, optional) : used to update aggregation data per grid, details in Output section. * maxCountBuffer: (Buffer, optional) : used to update total aggregation data, details in Output section. * projectPoints (Bool) : when true performs aggregation in screen space. -* gridTransformMatrix (Mat4) : used to transform input positions before aggregating them (for example, lng/lat can be moved to +ve range, when doing world space aggregation, projectPoints=false). +* translation (Array) : [xOffset, yOffset], used to translate input positions before aggregating them (for example, lng/lat can be moved to +ve range). +* scaling (Array) : [xScale, yScale, isScalingValid] : `xScale`, `yScale` define scaling to be applied before aggregating. Scaling is applied only when `isScalingValie` is > 0. * createBufferObjects (Bool, options, default: true) : Only applicable when aggregation is performed on CPU. When set to false, aggregated data is not uploaded into Buffer objects. In a typical use case, Applications need data in `Buffer` objects to use them in next rendering cycle, hence by default its value is true, but if needed this step can be avoided by setting this flag to false. +NOTE: When doing screen space aggregation, i.e projectPoints is true, `translation` and `scaling` should be set to transformation required for camera space (NDC) to screen (pixel) space. + Returns: * An object, where key represents `id` of the weight and value contains following aggregated data. diff --git a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/grid-aggregation-utils.js b/modules/aggregation-layers/src/utils/gpu-grid-aggregation/grid-aggregation-utils.js deleted file mode 100644 index 675fa66ebe4..00000000000 --- a/modules/aggregation-layers/src/utils/gpu-grid-aggregation/grid-aggregation-utils.js +++ /dev/null @@ -1,198 +0,0 @@ -import {Matrix4} from 'math.gl'; -import {COORDINATE_SYSTEM, log} from '@deck.gl/core'; - -const R_EARTH = 6378000; - -function toFinite(n) { - return Number.isFinite(n) ? n : 0; -} - -// Takes data and aggregation params and returns aggregated data. -export function pointToDensityGridData({ - cellSizeMeters, - gpuGridAggregator, - gpuAggregation, - aggregationFlags, - weightParams, - fp64 = false, - coordinateSystem = COORDINATE_SYSTEM.LNGLAT, - viewport = null, - boundingBox = null, - vertexCount, - attributes, - moduleSettings = {} -}) { - if (aggregationFlags.dataChanged) { - boundingBox = getBoundingBox(attributes, vertexCount); - } - log.assert(cellSizeMeters > 0); - let cellSize = [cellSizeMeters, cellSizeMeters]; - let worldOrigin = [0, 0]; - log.assert( - coordinateSystem === COORDINATE_SYSTEM.LNGLAT || coordinateSystem === COORDINATE_SYSTEM.IDENTITY - ); - - switch (coordinateSystem) { - case COORDINATE_SYSTEM.LNGLAT: - case COORDINATE_SYSTEM.LNGLAT_DEPRECATED: - const gridOffset = getGridOffset(boundingBox, cellSizeMeters); - cellSize = [gridOffset.xOffset, gridOffset.yOffset]; - worldOrigin = [-180, -90]; // Origin used to define grid cell boundaries - break; - case COORDINATE_SYSTEM.IDENTITY: - const {width, height} = viewport; - worldOrigin = [-width / 2, -height / 2]; // Origin used to define grid cell boundaries - break; - default: - // Currently other coodinate systems not supported/verified. - log.assert(false); - } - - const opts = getGPUAggregationParams({boundingBox, cellSize, worldOrigin}); - - const aggregatedData = gpuGridAggregator.run({ - weights: weightParams, - cellSize, - width: opts.width, - height: opts.height, - gridTransformMatrix: opts.gridTransformMatrix, - useGPU: gpuAggregation, - changeFlags: aggregationFlags, - fp64, - vertexCount, - attributes, - moduleSettings - }); - - return { - weights: aggregatedData, - gridSize: opts.gridSize, - gridOrigin: opts.gridOrigin, - cellSize, - boundingBox - }; -} - -// Parse input data to build positions, wights and bounding box. -/* eslint-disable max-statements */ -function getBoundingBox(attributes, vertexCount) { - // TODO - value might not exist (e.g. attribute transition) - const positions = attributes.positions.value; - - let yMin = Infinity; - let yMax = -Infinity; - let xMin = Infinity; - let xMax = -Infinity; - let y; - let x; - - for (let i = 0; i < vertexCount; i++) { - x = positions[i * 3]; - y = positions[i * 3 + 1]; - yMin = y < yMin ? y : yMin; - yMax = y > yMax ? y : yMax; - xMin = x < xMin ? x : xMin; - xMax = x > xMax ? x : xMax; - } - - const boundingBox = { - xMin: toFinite(xMin), - xMax: toFinite(xMax), - yMin: toFinite(yMin), - yMax: toFinite(yMax) - }; - - return boundingBox; -} -/* eslint-enable max-statements */ - -/** - * Based on geometric center of sample points, calculate cellSize in lng/lat (degree) space - * @param {object} gridData - contains bounding box of data - * @param {number} cellSize - grid cell size in meters - * @returns {yOffset, xOffset} - cellSize size lng/lat (degree) space. - */ - -function getGridOffset(boundingBox, cellSize) { - const {yMin, yMax} = boundingBox; - const latMin = yMin; - const latMax = yMax; - const centerLat = (latMin + latMax) / 2; - - return calculateGridLatLonOffset(cellSize, centerLat); -} - -/** - * calculate grid layer cell size in lat lon based on world unit size - * and current latitude - * @param {number} cellSize - * @param {number} latitude - * @returns {object} - lat delta and lon delta - */ -function calculateGridLatLonOffset(cellSize, latitude) { - const yOffset = calculateLatOffset(cellSize); - const xOffset = calculateLonOffset(latitude, cellSize); - return {yOffset, xOffset}; -} - -/** - * with a given x-km change, calculate the increment of latitude - * based on stackoverflow http://stackoverflow.com/questions/7477003 - * @param {number} dy - change in km - * @return {number} - increment in latitude - */ -function calculateLatOffset(dy) { - return (dy / R_EARTH) * (180 / Math.PI); -} - -/** - * with a given x-km change, and current latitude - * calculate the increment of longitude - * based on stackoverflow http://stackoverflow.com/questions/7477003 - * @param {number} lat - latitude of current location (based on city) - * @param {number} dx - change in km - * @return {number} - increment in longitude - */ -function calculateLonOffset(lat, dx) { - return ((dx / R_EARTH) * (180 / Math.PI)) / Math.cos((lat * Math.PI) / 180); -} - -// Aligns `inValue` to given `cellSize` -export function alignToCell(inValue, cellSize) { - const sign = inValue < 0 ? -1 : 1; - - let value = sign < 0 ? Math.abs(inValue) + cellSize : Math.abs(inValue); - - value = Math.floor(value / cellSize) * cellSize; - - return value * sign; -} - -// Calculate grid parameters -function getGPUAggregationParams({boundingBox, cellSize, worldOrigin}) { - const {yMin, yMax, xMin, xMax} = boundingBox; - - // NOTE: this alignment will match grid cell boundaries with existing CPU implementation - // this gurantees identical aggregation results when switching between CPU and GPU aggregation. - // Also gurantees same cell boundaries, when overlapping between two different layers (like ScreenGrid and Contour) - // We first move worldOrigin to [0, 0], align the lower bounding box , then move worldOrigin to its original value. - const originX = alignToCell(xMin - worldOrigin[0], cellSize[0]) + worldOrigin[0]; - const originY = alignToCell(yMin - worldOrigin[1], cellSize[1]) + worldOrigin[1]; - - // Setup transformation matrix so that every point is in +ve range - const gridTransformMatrix = new Matrix4().translate([-1 * originX, -1 * originY, 0]); - - const gridOrigin = [originX, originY]; - const width = xMax - xMin + cellSize[0]; - const height = yMax - yMin + cellSize[1]; - - const gridSize = [Math.ceil(width / cellSize[0]), Math.ceil(height / cellSize[1])]; - - return { - gridOrigin, - gridSize, - width, - height, - gridTransformMatrix - }; -} diff --git a/modules/aggregation-layers/src/utils/grid-aggregation-utils.js b/modules/aggregation-layers/src/utils/grid-aggregation-utils.js new file mode 100644 index 00000000000..a32d1c2bd22 --- /dev/null +++ b/modules/aggregation-layers/src/utils/grid-aggregation-utils.js @@ -0,0 +1,47 @@ +function toFinite(n) { + return Number.isFinite(n) ? n : 0; +} + +// Parse input data to build positions, wights and bounding box. +/* eslint-disable max-statements */ +export function getBoundingBox(attributes, vertexCount) { + // TODO - value might not exist (e.g. attribute transition) + const positions = attributes.positions.value; + + let yMin = Infinity; + let yMax = -Infinity; + let xMin = Infinity; + let xMax = -Infinity; + let y; + let x; + + for (let i = 0; i < vertexCount; i++) { + x = positions[i * 3]; + y = positions[i * 3 + 1]; + yMin = y < yMin ? y : yMin; + yMax = y > yMax ? y : yMax; + xMin = x < xMin ? x : xMin; + xMax = x > xMax ? x : xMax; + } + + const boundingBox = { + xMin: toFinite(xMin), + xMax: toFinite(xMax), + yMin: toFinite(yMin), + yMax: toFinite(yMax) + }; + + return boundingBox; +} +/* eslint-enable max-statements */ + +// Aligns `inValue` to given `cellSize` +export function alignToCell(inValue, cellSize) { + const sign = inValue < 0 ? -1 : 1; + + let value = sign < 0 ? Math.abs(inValue) + cellSize : Math.abs(inValue); + + value = Math.floor(value / cellSize) * cellSize; + + return value * sign; +} diff --git a/test/browser.js b/test/browser.js index 0c0393e148f..5e1ed98cf1e 100644 --- a/test/browser.js +++ b/test/browser.js @@ -48,7 +48,8 @@ test('deck.gl', t => { require('./modules/json/json-render.spec'); require('./modules/main/bundle'); require('./modules/aggregation-layers/utils/gpu-grid-aggregator.spec'); - require('./modules/aggregation-layers/utils/grid-aggregation-utils.spec'); + require('./modules/aggregation-layers/gpu-grid-layer/gpu-grid-layer.spec'); + require('./modules/aggregation-layers/grid-aggregation-layer.spec'); require('./modules/aggregation-layers/heatmap-layer/heatmap-layer.spec'); require('./modules/core/lib/pick-layers.spec'); diff --git a/test/data/grid-aggregation-data.js b/test/data/grid-aggregation-data.js index 1c5c22cd1a0..547137d338f 100644 --- a/test/data/grid-aggregation-data.js +++ b/test/data/grid-aggregation-data.js @@ -20,7 +20,7 @@ const viewportUpdated = new WebMercatorViewport( Object.assign({}, viewportProps, {zoom: viewportProps.zoom - 3}) ); -function buildDataProp(opts) { +export function buildDataProp(opts) { const pointCount = opts.positions.length / 2; const dataProp = []; for (let i = 0; i < pointCount; i++) { @@ -34,7 +34,7 @@ function buildDataProp(opts) { return dataProp; } -function buildAttributes(opts) { +export function buildAttributes(opts) { const {weights} = opts; const data = opts.data || buildDataProp(opts); @@ -108,7 +108,7 @@ const fixture = { 1, 500 // gets aggregated when viewport is zoomed out ], - size: 3, + size: 1, needMin: true, needMax: true } @@ -120,7 +120,9 @@ const fixture = { dataChanged: true }, moduleSettings: {viewport}, - projectPoints: true + projectPoints: true, + translation: [1, -1], + scaling: [viewport.width / 2, -viewport.height / 2, 1] }; Object.assign(fixture, buildAttributes(fixture)); @@ -231,7 +233,6 @@ function generateRandomGridPoints(pointCount) { const weights = { weight1: { values: weightValues, - size: 3, needMin: true, needMax: true } diff --git a/test/modules/aggregation-layers/aggregation-layer.spec.js b/test/modules/aggregation-layers/aggregation-layer.spec.js index 349923a540f..fdb56999f47 100644 --- a/test/modules/aggregation-layers/aggregation-layer.spec.js +++ b/test/modules/aggregation-layers/aggregation-layer.spec.js @@ -55,9 +55,9 @@ class TestAggregationLayer extends AggregationLayer { // clear state this.setState({aggregationDirty: false}); super.updateState(opts); - this.setState({aggregationDirty: this._isAggregationDirty(opts)}); + this.setState({aggregationDirty: this.isAggregationDirty(opts)}); } - _updateShaders(shaderOptions) {} + updateShaders(shaderOptions) {} // // updateAttributes(changedAttributes) {} } @@ -90,12 +90,12 @@ test('AggregationLayer#updateState', t => { updateProps: { prop1: 20 }, - spies: ['_updateShaders', 'updateAttributes'], + spies: ['updateShaders', 'updateAttributes'], onAfterUpdate({spies, layer}) { t.ok(spies.updateAttributes.called, 'should always call updateAttributes'); t.notOk( - spies._updateShaders.called, - 'should not call _updateShaders when extensions not changed' + spies.updateShaders.called, + 'should not call updateShaders when extensions not changed' ); t.notOk(layer.state.aggregationDirty, 'Aggregation should not be dirty'); } @@ -104,7 +104,7 @@ test('AggregationLayer#updateState', t => { updateProps: { cellSize: 21 }, - spies: ['_updateShaders', 'updateAttributes'], + spies: ['updateShaders', 'updateAttributes'], onAfterUpdate({layer}) { t.ok( layer.state.aggregationDirty, @@ -116,9 +116,9 @@ test('AggregationLayer#updateState', t => { updateProps: { extensions: [new DataFilterExtension({filterSize: 2})] // default value is true }, - spies: ['_updateShaders'], + spies: ['updateShaders'], onAfterUpdate({spies, layer}) { - t.ok(spies._updateShaders.called, 'should call _updateShaders when extensions changed'); + t.ok(spies.updateShaders.called, 'should call updateShaders when extensions changed'); t.ok(layer.state.aggregationDirty, 'Aggregation should be dirty when extensions changed'); } }, diff --git a/test/modules/aggregation-layers/contour-layer/contour-layer.spec.js b/test/modules/aggregation-layers/contour-layer/contour-layer.spec.js index 76f923705c7..a52c19438b1 100644 --- a/test/modules/aggregation-layers/contour-layer/contour-layer.spec.js +++ b/test/modules/aggregation-layers/contour-layer/contour-layer.spec.js @@ -28,6 +28,16 @@ import {LineLayer, SolidPolygonLayer} from '@deck.gl/layers'; import {ContourLayer} from '@deck.gl/aggregation-layers'; const getPosition = d => d.COORDINATES; +const CONTOURS1 = [ + {threshold: 1, color: [255, 0, 0]}, // => Isoline for threshold 1 + {threshold: 5, color: [0, 255, 0]}, // => Isoline for threshold 5 + {threshold: [6, 10], color: [0, 0, 255]} // => Isoband for threshold range [6, 10) +]; +const CONTOURS2 = [ + // contours count changed + {threshold: 5, color: [0, 255, 0]}, + {threshold: [6, 10], color: [0, 0, 255]} +]; test('ContourLayer', t => { const testCases = generateLayerTests({ @@ -39,7 +49,10 @@ test('ContourLayer', t => { assert: t.ok, onBeforeUpdate: ({testCase}) => t.comment(testCase.title), onAfterUpdate({layer}) { - t.ok(layer.state.countsData, 'should update state.countsData'); + if (layer.getNumInstances() > 0) { + const {aggregationData} = layer.state.weights.count; + t.ok(aggregationData, 'should create aggregationData'); + } } }); @@ -54,11 +67,7 @@ test('ContourLayer#renderSubLayer', t => { const layer = new ContourLayer({ id: 'contourLayer', data: FIXTURES.points, - contours: [ - {threshold: 1, color: [255, 0, 0]}, // => Isoline for threshold 1 - {threshold: 5, color: [0, 255, 0]}, // => Isoline for threshold 5 - {threshold: [6, 10], color: [0, 0, 255]} // => Isoband for threshold range [6, 10) - ], + contours: CONTOURS1, cellSize: 200, getPosition }); @@ -88,18 +97,15 @@ test('ContourLayer#updates', t => { props: { data: FIXTURES.points, cellSize: 400, - contours: [ - {threshold: 1, color: [255, 0, 0]}, // => Isoline for threshold 1 - {threshold: 5, color: [0, 255, 0]}, // => Isoline for threshold 5 - {threshold: [6, 10], color: [0, 0, 255]} // => Isoband for threshold range [6, 10) - ], + contours: CONTOURS1, getPosition, pickable: true }, onAfterUpdate({layer}) { - const {countsData, contourData, thresholdData} = layer.state; + const {aggregationData} = layer.state.weights.count; + const {contourData, thresholdData} = layer.state; - t.ok(countsData.length > 0, 'ContourLayer data is aggregated'); + t.ok(aggregationData.length > 0, 'ContourLayer data is aggregated'); t.ok( Array.isArray(contourData.contourSegments) && contourData.contourSegments.length > 1, 'ContourLayer iso-lines calculated' @@ -119,104 +125,73 @@ test('ContourLayer#updates', t => { updateProps: { gpuAggregation: false // default value is true }, - spies: ['_aggregateData'], - onAfterUpdate({spies}) { - t.ok(spies._aggregateData.called, 'should re-aggregate data on gpuAggregation change'); + spies: ['_updateAggregation'], + onAfterUpdate({spies, layer, oldState}) { + if (oldState.gpuAggregation) { + // Under WebGL1, gpuAggregation will be false + t.ok( + spies._updateAggregation.called, + 'should re-aggregate data on gpuAggregation change' + ); + } } }, { updateProps: { cellSize: 500 // changed from 400 to 500 }, - spies: ['_onGetSublayerColor', '_aggregateData', '_generateContours'], + spies: ['_onGetSublayerColor', '_updateAggregation', '_generateContours'], onAfterUpdate({layer, subLayers, spies}) { t.ok(subLayers.length === 2, 'Sublayers rendered'); - t.ok(spies._aggregateData.called, 'should re-aggregate data on cellSize change'); + t.ok(spies._updateAggregation.called, 'should re-aggregate data on cellSize change'); t.ok(spies._generateContours.called, 'should re-generate contours on cellSize change'); t.ok( spies._onGetSublayerColor.called, 'should call _onGetSublayerColor on cellSize change' ); - spies._aggregateData.restore(); + spies._updateAggregation.restore(); spies._generateContours.restore(); spies._onGetSublayerColor.restore(); } }, { updateProps: { - contours: [ - // contours count changed - {threshold: 5, color: [0, 255, 0]}, - {threshold: [6, 10], color: [0, 0, 255]} - ] + contours: CONTOURS2 }, - spies: ['_updateThresholdData', '_generateContours', '_aggregateData'], + spies: [ + '_updateThresholdData', + '_generateContours', + '_updateAggregation', + '_onGetSublayerStrokeWidth', + '_onGetSublayerColor' + ], onAfterUpdate({subLayers, spies}) { t.ok(subLayers.length === 2, 'Sublayers rendered'); t.ok( spies._updateThresholdData.called, - 'should update threshold data on countours count change' - ); - t.ok( - spies._generateContours.called, - 'should re-generate contours on countours count change' + 'should update threshold data on countours change' ); + t.ok(spies._generateContours.called, 'should re-generate contours on countours change'); t.ok( - !spies._aggregateData.called, + !spies._updateAggregation.called, 'should NOT re-aggregate data on countours count change' ); - spies._updateThresholdData.restore(); - spies._generateContours.restore(); - spies._aggregateData.restore(); - } - }, - { - updateProps: { - contours: [ - // threshold value changed - {threshold: 5, color: [0, 255, 0]}, - {threshold: [6, 50], color: [0, 0, 255]} // changed [6, 10] to [6, 50] - ] - }, - spies: ['_updateThresholdData', '_generateContours'], - onAfterUpdate({subLayers, spies}) { - t.ok(subLayers.length === 2, 'Sublayers rendered'); - t.ok( - spies._updateThresholdData.called, - 'should update threshold data on threshold value change' + spies._onGetSublayerColor.called, + 'should call _onGetSublayerColor on contours change' ); t.ok( - spies._generateContours.called, - 'should re-generate contours on threshold value change' + spies._onGetSublayerStrokeWidth.called, + 'should call _onGetSublayerStrokeWidth on contours change' ); + spies._updateThresholdData.restore(); spies._generateContours.restore(); - } - }, - { - updateProps: { - contours: [ - // threshold color changed - {threshold: 5, color: [255, 0, 0]}, // color changed from Green to Red - {threshold: [6, 50], color: [0, 0, 255]} - ] - }, - spies: ['_onGetSublayerColor', '_generateContours', '_aggregateData'], - onAfterUpdate({subLayers, spies}) { - t.ok(subLayers.length === 2, 'Sublayers rendered'); - - t.ok(spies._onGetSublayerColor.called, 'should update color on threshold color change'); - t.ok( - !spies._generateContours.called, - 'should NOT generate contours on threshold color change' - ); - t.ok(!spies._aggregateData.called, 'should NOT aggregate data on threshold color change'); + spies._updateAggregation.restore(); spies._onGetSublayerColor.restore(); - spies._generateContours.restore(); - spies._aggregateData.restore(); + spies._onGetSublayerStrokeWidth.restore(); } } ] diff --git a/test/modules/aggregation-layers/gpu-grid-layer/gpu-grid-layer.spec.js b/test/modules/aggregation-layers/gpu-grid-layer/gpu-grid-layer.spec.js index e8d279297da..e51d8c21771 100644 --- a/test/modules/aggregation-layers/gpu-grid-layer/gpu-grid-layer.spec.js +++ b/test/modules/aggregation-layers/gpu-grid-layer/gpu-grid-layer.spec.js @@ -53,7 +53,7 @@ test('GPUGridLayer', t => { test('GPUGridLayer#renderLayers', t => { const webgl1Spies = setupSpysForWebGL1(gl); - makeSpy(GPUGridLayer.prototype, '_aggregateData'); + makeSpy(GPUGridLayer.prototype, '_updateAggregation'); const layer = new GPUGridLayer(SAMPLE_PROPS); @@ -65,8 +65,8 @@ test('GPUGridLayer#renderLayers', t => { t.ok(sublayer instanceof GPUGridCellLayer, 'Sublayer GPUGridCellLayer layer rendered'); - t.ok(GPUGridLayer.prototype._aggregateData.called, 'should call _aggregateData'); - GPUGridLayer.prototype._aggregateData.restore(); + t.ok(GPUGridLayer.prototype._updateAggregation.called, 'should call _updateAggregation'); + GPUGridLayer.prototype._updateAggregation.restore(); restoreSpies(webgl1Spies); t.end(); @@ -81,21 +81,10 @@ test('GPUGridLayer#updates', t => { { props: SAMPLE_PROPS, onAfterUpdate({layer}) { - const {weights, gridSize, gridOrigin, cellSize, boundingBox} = layer.state; + const {weights, numCol, numRow, boundingBox} = layer.state; t.ok(weights.color.aggregationBuffer, 'Data is aggregated'); - t.ok( - Number.isFinite(gridSize[0]) && Number.isFinite(gridSize[1]), - 'gridSize is calculated' - ); - t.ok( - Number.isFinite(gridOrigin[0]) && Number.isFinite(gridOrigin[1]), - 'gridOrigin is calculated' - ); - t.ok( - Number.isFinite(cellSize[0]) && Number.isFinite(cellSize[1]), - 'cellSize is calculated' - ); + t.ok(numCol && numRow, 'gridSize is calculated'); t.ok( Number.isFinite(boundingBox.xMin) && Number.isFinite(boundingBox.xMax), 'boundingBox is calculated' @@ -106,33 +95,33 @@ test('GPUGridLayer#updates', t => { updateProps: { colorRange: GPUGridLayer.defaultProps.colorRange.slice() }, - spies: ['_aggregateData'], + spies: ['_updateAggregation'], onAfterUpdate({layer, subLayers, spies}) { - t.notOk(spies._aggregateData.called, 'should not call _aggregateData'); + t.notOk(spies._updateAggregation.called, 'should not call _updateAggregation'); - spies._aggregateData.restore(); + spies._updateAggregation.restore(); } }, { updateProps: { cellSize: 10 }, - spies: ['_aggregateData'], + spies: ['_updateAggregation'], onAfterUpdate({layer, subLayers, spies}) { - t.ok(spies._aggregateData.called, 'should call _aggregateData'); + t.ok(spies._updateAggregation.called, 'should call _updateAggregation'); - spies._aggregateData.restore(); + spies._updateAggregation.restore(); } }, { updateProps: { colorAggregation: 3 }, - spies: ['_aggregateData'], + spies: ['_updateAggregation'], onAfterUpdate({layer, subLayers, spies}) { - t.ok(spies._aggregateData.called, 'should call _aggregateData'); + t.ok(spies._updateAggregation.called, 'should call _updateAggregation'); - spies._aggregateData.restore(); + spies._updateAggregation.restore(); } } ] diff --git a/test/modules/aggregation-layers/grid-aggregation-layer.spec.js b/test/modules/aggregation-layers/grid-aggregation-layer.spec.js new file mode 100644 index 00000000000..85371c5cb92 --- /dev/null +++ b/test/modules/aggregation-layers/grid-aggregation-layer.spec.js @@ -0,0 +1,278 @@ +// Copyright (c) 2015 - 2019 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import test from 'tape-catch'; +import GridAggregationLayer from '@deck.gl/aggregation-layers/grid-aggregation-layer'; +import GL from '@luma.gl/constants'; +import {Layer} from 'deck.gl'; +import {testLayer} from '@deck.gl/test-utils'; +import {GridAggregationData} from 'deck.gl-test/data'; +import {equals} from 'math.gl'; +import {Buffer} from '@luma.gl/core'; + +const BASE_LAYER_ID = 'composite-layer-id'; +const PROPS = { + cellSize: 10, + prop1: 5 +}; + +class TestLayer extends Layer { + initializeState() {} +} + +TestLayer.layerName = 'TestLayer'; + +const AGGREGATION_PROPS = ['cellSize']; + +class TestGridAggregationLayer extends GridAggregationLayer { + initializeState() { + const {gl} = this.context; + super.initializeState({aggregationProps: AGGREGATION_PROPS}); + this.setState({ + weights: { + count: { + needMin: true, + needMax: true, + size: 1, + minBuffer: new Buffer(gl, { + byteLength: 4 * 4, + accessor: {size: 4, type: GL.FLOAT, divisor: 1} + }), + maxBuffer: new Buffer(gl, { + byteLength: 4 * 4, + accessor: {size: 4, type: GL.FLOAT, divisor: 1} + }) + } + } + }); + const attributeManager = this.getAttributeManager(); + attributeManager.add({ + positions: { + size: 3, + accessor: 'getPosition', + type: GL.DOUBLE, + fp64: this.use64bitPositions() + }, + // this attribute is used in gpu aggregation path only + count: {size: 3, accessor: 'getWeight'} + }); + } + + renderLayers() { + return [ + new TestLayer(this.getSubLayerProps(), { + scale: this.state.scale + }) + ]; + } + + updateState(opts) { + super.updateState(opts); + } + + finalizeState() { + super.finalizeState(); + const {minBuffer, maxBuffer} = this.state.weights.count; + minBuffer.delete(); + maxBuffer.delete(); + } + + _getGridOffset(opts) { + const {cellSize, screenSpaceAggregation} = this.state; + if (!screenSpaceAggregation) { + return super._getGridOffset(opts); + } + return {xOffset: cellSize, yOffset: cellSize}; + } + + updateAggregationFlags(opts) { + const cellSizeChanged = opts.oldProps.cellSize !== opts.props.cellSize; + const gpuAggregation = opts.props.gpuAggregation; + const gpuAggregationChanged = gpuAggregation !== this.state.gpuAggregation; + // Consider switching between CPU and GPU aggregation as data changed as it requires + // re aggregation. + const dataChanged = this.state.dataChanged || gpuAggregationChanged; + this.setState({ + dataChanged, + cellSizeChanged, + cellSize: opts.props.cellSize, + needsReProjection: dataChanged || cellSizeChanged, + gpuAggregation + }); + } + + // capture results + updateResults(results) { + this.setState({cpuResults: results}); + } +} +TestGridAggregationLayer.defaultProps = { + cellSize: {type: 'number', min: 1, max: 1000, value: 1000}, + getPosition: {type: 'accessor', value: x => x.position}, + getWeight: {type: 'accessor', value: x => 1}, + gpuAggregation: true, + aggregation: 'SUM' +}; +TestGridAggregationLayer.layerName = 'TestGridAggregationLayer'; + +const {fixture} = GridAggregationData; + +test('GridAggregationLayer#constructor', t => { + const layer = new TestGridAggregationLayer(Object.assign({id: BASE_LAYER_ID}, PROPS)); + t.ok(layer, 'AggregationLayer created'); + t.end(); +}); + +function verifyAggregationResults(t, {layer, gpu}) { + const {cpuResults} = layer.state; + let gpuResults; + if (gpu) { + const {aggregationBuffer, minBuffer, maxBuffer} = layer.state.weights.count; + gpuResults = { + aggregationData: aggregationBuffer.getData(), + minData: minBuffer.getData(), + maxData: maxBuffer.getData() + }; + layer.setState({gpuResults}); + } + t.ok( + gpu ? gpuResults.aggregationData : cpuResults.aggregationData, + 'should calculate aggregationData' + ); +} + +function compareAggregationResults(t, layer) { + const {cpuResults, gpuResults} = layer.state; + t.ok( + equals( + filterEmptyChannels(cpuResults.aggregationData), + filterEmptyChannels(gpuResults.aggregationData), + 1e-6 + ), + 'aggregationData should match' + ); + t.ok( + equals(filterEmptyChannels(cpuResults.minData), filterEmptyChannels(gpuResults.minData)), + 'minData should match' + ); + t.ok( + equals(filterEmptyChannels(cpuResults.maxData), filterEmptyChannels(gpuResults.maxData)), + 'maxData should match' + ); + + // DEBUG + // logNonZero('cpu : aggregation', cpuResults.aggregationData); + // logNonZero('gpu : aggregation', gpuResults.aggregationData); + // logNonZero('cpu : min', cpuResults.minData); + // logNonZero('gpu : min', gpuResults.minData); + // logNonZero('cpu : max', cpuResults.maxData); + // logNonZero('gpu : max', gpuResults.maxData); + + // clear cpuResults + layer.setState({cpuResults: null, gpuResults: null}); +} + +function getTestCases(t, updateProps) { + return [ + { + updateProps: Object.assign({}, updateProps, {gpuAggregation: false}), + onAfterUpdate({layer}) { + verifyAggregationResults(t, {layer, gpu: false}); + } + }, + { + updateProps: Object.assign({gpuAggregation: true}), + onAfterUpdate({layer}) { + verifyAggregationResults(t, {layer, gpu: true}); + compareAggregationResults(t, layer); + } + } + ]; +} + +test('GridAggregationLayer#CPUvsGPUAggregation', t => { + let testCases = [ + { + props: { + data: fixture.data, + getWeight: x => x.weight1[0], + aggregation: 'SUM', + cellSize: 500, + gpuAggregation: false + }, + onAfterUpdate({layer}) { + verifyAggregationResults(t, {layer, gpu: false}); + } + }, + { + updateProps: { + gpuAggregation: true + }, + onAfterUpdate({layer}) { + verifyAggregationResults(t, {layer, gpu: true}); + compareAggregationResults(t, layer); + } + } + ]; + + testCases = [ + ...testCases, + ...getTestCases(t, {cellSize: 600, aggregation: 'MAX'}) + // Takes too long on CI + // ...getTestCases(t, {aggregation: 'MAX'}), + // ...getTestCases(t, {aggregation: 'MIN'}), + // ...getTestCases(t, {aggregation: 'MEAN'}) + ]; + + testLayer({ + Layer: TestGridAggregationLayer, + onError: t.notOk, + viewport: fixture.moduleSettings.viewport, + testCases + }); + + t.end(); +}); + +function filterEmptyChannels(inArray) { + const outArray = []; + for (let i = 0; i < inArray.length; i += 4) { + if (inArray[i + 3] > 0) { + // something is being rendered to this pixel + outArray.push(inArray[i], inArray[i + 3]); + } + } + return outArray; +} + +// DEBUG ONLY +/* eslint-disable no-console, no-undef */ +// function logNonZero(name, array) { +// console.log(`${name}: length: ${array.length}`); +// for (let i = 0; i < array.length; i += 4) { +// // if (i*4 === 4143988) { +// // console.log(`==> ${i}: ${array[i*4]} ${array[i*4 + 1]} ${array[i*4 + 2]} ${array[i*4 + 3]}`); +// // } +// if (array[i + 3] > 0) { +// console.log(`${i}: ${array[i]} ${array[i + 1]} ${array[i + 2]} ${array[i + 3]}`); +// } +// } +// } +/* eslint-enable no-console, no-undef */ diff --git a/test/modules/aggregation-layers/index.js b/test/modules/aggregation-layers/index.js index cdf14ad7cbe..25782942600 100644 --- a/test/modules/aggregation-layers/index.js +++ b/test/modules/aggregation-layers/index.js @@ -21,7 +21,6 @@ import './contour-layer/contour-layer.spec'; import './contour-layer/marching-squares.spec'; import './gpu-grid-layer/gpu-grid-cell-layer-vertex.spec'; -import './gpu-grid-layer/gpu-grid-layer.spec'; import './gpu-grid-layer/gpu-grid-cell-layer.spec'; import './cpu-grid-layer/cpu-grid-layer.spec'; import './grid-aggregator.spec'; diff --git a/test/modules/aggregation-layers/screen-grid-layer.spec.js b/test/modules/aggregation-layers/screen-grid-layer.spec.js index 1db345b558e..401d7431663 100644 --- a/test/modules/aggregation-layers/screen-grid-layer.spec.js +++ b/test/modules/aggregation-layers/screen-grid-layer.spec.js @@ -49,11 +49,12 @@ test('ScreenGridLayer', t => { updateProps: { gpuAggregation: false }, - spies: ['_updateAggregation'], - onAfterUpdate({layer, subLayers, spies}) { - t.ok(spies._updateAggregation.called, 'should call _aggregateData'); - - spies._updateAggregation.restore(); + onAfterUpdate({layer, oldState}) { + if (oldState.gpuAggregation) { + // Under WebGL1 gpuAggregation is always false, this change is a nop + const {dataChanged} = layer.state; + t.ok(dataChanged, 'should set dataChanged when gpuAggregation prop changes'); + } } } ]); diff --git a/test/modules/aggregation-layers/utils/gpu-grid-aggregator.spec.js b/test/modules/aggregation-layers/utils/gpu-grid-aggregator.spec.js index bd69c5313f6..904a26a77fb 100644 --- a/test/modules/aggregation-layers/utils/gpu-grid-aggregator.spec.js +++ b/test/modules/aggregation-layers/utils/gpu-grid-aggregator.spec.js @@ -1,27 +1,24 @@ import test from 'tape-catch'; import GPUGridAggregator from '@deck.gl/aggregation-layers/utils/gpu-grid-aggregation/gpu-grid-aggregator'; -import {AGGREGATION_OPERATION} from '@deck.gl/aggregation-layers/utils/aggregation-operation-utils'; +import { + AGGREGATION_OPERATION, + getValueFunc +} from '@deck.gl/aggregation-layers/utils/aggregation-operation-utils'; +import {pointToDensityGridDataCPU} from '@deck.gl/aggregation-layers/cpu-grid-layer/grid-aggregator'; +import BinSorter from '@deck.gl/aggregation-layers/utils/bin-sorter'; + import {gl} from '@deck.gl/test-utils'; import {GridAggregationData} from 'deck.gl-test/data'; import {equals, config} from 'math.gl'; -const {fixture, fixtureUpdated, fixtureWorldSpace} = GridAggregationData; - -function getCPUResults({aggregationData, minData, maxData}) { - return {aggregationData, minData, maxData}; -} - -function getGPUResults({aggregationBuffer, minBuffer, maxBuffer}) { - return { - aggregationData: aggregationBuffer.getData(), - minData: minBuffer.getData(), - maxData: maxBuffer.getData() - }; -} +const {fixture, buildAttributes} = GridAggregationData; function verifyResults({t, cpuResults, gpuResults, testName}) { for (const name in cpuResults) { - if (equals(cpuResults[name], gpuResults[name])) { + if ( + equals(cpuResults[name][0], gpuResults[name][0]) && + equals(cpuResults[name][3], gpuResults[name][3]) + ) { t.pass(`${testName}: ${name} CPU and GPU results matched`); } else { t.fail( @@ -35,15 +32,13 @@ function verifyResults({t, cpuResults, gpuResults, testName}) { /* eslint-disable max-statements */ function testCounterMinMax(aggregator, t, opts) { - const {useGPU, size = 1} = opts; - const testName = `${useGPU ? 'GPU' : 'CPU'} size: ${size}:`; + const {size = 1} = opts; + const testName = `GPU : size: ${size}:`; let weight1 = Object.assign({}, fixture.weights.weight1, {size}); - let results = aggregator.run(Object.assign({}, fixture, {weights: {weight1}, useGPU})); - // GPUGridAggregator.logData(results.weight1); + let results = aggregator.run(Object.assign({}, fixture, {weights: {weight1}})); - const minData = results.weight1.minBuffer.getData(); - const maxData = results.weight1.maxBuffer.getData(); + const {minData, maxData} = aggregator.getData('weight1'); t.equal(maxData[3], 3, `${testName} needMax: total count should match`); t.equal(minData[3], 3, `${testName} needMin: total count should match`); t.equal(maxData[0], 4, `${testName} needMax: max weight should match`); @@ -58,7 +53,7 @@ function testCounterMinMax(aggregator, t, opts) { } weight1 = Object.assign({}, weight1, {combineMaxMin: true}); - results = aggregator.run(Object.assign({}, fixture, {weights: {weight1}, useGPU})); + results = aggregator.run(Object.assign({}, fixture, {weights: {weight1}})); const maxMinData = results.weight1.maxMinBuffer.getData(); t.equal(maxMinData[0], 4, `${testName} combineMaxMin: max weight should match`); @@ -78,39 +73,28 @@ function testCounterMinMax(aggregator, t, opts) { test('GPUGridAggregator#GPU', t => { const sa = new GPUGridAggregator(gl); - testCounterMinMax(sa, t, {useGPU: true}); - testCounterMinMax(sa, t, {useGPU: true, size: 2}); - testCounterMinMax(sa, t, {useGPU: true, size: 3}); + testCounterMinMax(sa, t, {size: 1}); + testCounterMinMax(sa, t, {size: 2}); + testCounterMinMax(sa, t, {size: 3}); t.end(); }); const {generateRandomGridPoints} = GridAggregationData; -test('GPUGridAggregator#CPU', t => { - const sa = new GPUGridAggregator(gl); - testCounterMinMax(sa, t, {useGPU: false}); - testCounterMinMax(sa, t, {useGPU: false, size: 2}); - testCounterMinMax(sa, t, {useGPU: false, size: 3}); - t.end(); -}); + +function cpuAggregator(props, aggregationParms) { + const layerData = pointToDensityGridDataCPU(props, aggregationParms); + // const {getWeight} = opts.weights.weight1; + const {aggregation} = aggregationParms; + const getValue = getValueFunc(aggregation, x => x.weight1[0]); + const {minValue, maxValue, totalCount} = new BinSorter(layerData.data, getValue, false); + const maxMinData = new Float32Array([maxValue, 0, 0, minValue]); + const maxData = new Float32Array([maxValue, 0, 0, totalCount]); + const minData = new Float32Array([minValue, 0, 0, totalCount]); + return {minData, maxData, maxMinData}; +} function testAggregationOperations(opts) { - const filterCellsWithNoData = (inArray, op) => { - if (op !== AGGREGATION_OPERATION.MIN) { - return inArray; - } - // When aggregation operation is MIN cpu and gpu will have different values - // for the cells which do not contain any sample points. In cpu weights will be 0, - // for gpu weights will be max float values. This is due to cpu/gpu aggregation implementation differences - // when operation is MIN. For testing purposes, discard all such cells. - const outArray = []; - for (let i = 0; i < inArray.length; i += 4) { - if (inArray[i + 3] > 0) { - outArray.push(inArray.slice(i, i + 4)); - } - } - return outArray; - }; - const {t, op, testName, pointsData} = opts; + const {t, op, aggregation, pointsData} = opts; const oldEpsilon = config.EPSILON; if (op === AGGREGATION_OPERATION.MEAN) { // cpu: 4.692307472229004 VS gpu: 4.692307949066162 @@ -118,230 +102,64 @@ function testAggregationOperations(opts) { config.EPSILON = 1e-6; } - const aggregator = new GPUGridAggregator(gl); + const gpuAggregator = new GPUGridAggregator(gl); const weight = Object.assign({}, pointsData.weights.weight1, {operation: op}); const maxMinweight = Object.assign({}, weight, {combineMaxMin: true}); - let results = aggregator.run( - Object.assign({}, fixture, {useGPU: false}, pointsData, {weights: {weight1: weight}}) + const aggregationOpts = Object.assign( + { + aggregation, + viewport: fixture.moduleSettings.viewport, + gridOffset: {xOffset: fixture.cellSize[0], yOffset: fixture.cellSize[1]}, + cellOffset: [0, 0] + }, + fixture, + pointsData, + { + weights: {weight1: weight} + } ); - const cpuResults = { - aggregationData: filterCellsWithNoData(results.weight1.aggregationBuffer.getData(), op), - minData: results.weight1.minBuffer.getData(), - maxData: results.weight1.maxBuffer.getData() - }; - results = aggregator.run( - Object.assign({}, fixture, {useGPU: false}, pointsData, {weights: {weight1: maxMinweight}}) + // const props = Object.assign({}, fixture, pointsData); + const cpuResults = cpuAggregator( + { + data: pointsData.data, + cellSize: fixture.cellSize + }, + { + aggregation, + viewport: fixture.moduleSettings.viewport, + gridOffset: {xOffset: fixture.cellSize[0], yOffset: fixture.cellSize[1]}, + cellOffset: [0, 0], + attributes: pointsData.attributes, + projectPoints: fixture.projectPoints, + numInstances: pointsData.vertexCount + } ); - cpuResults.maxMinData = results.weight1.maxMinBuffer.getData(); + let results = gpuAggregator.run(aggregationOpts); - results = aggregator.run( - Object.assign({}, fixture, {useGPU: true}, pointsData, {weights: {weight1: weight}}) - ); const gpuResults = { - aggregationData: filterCellsWithNoData(results.weight1.aggregationBuffer.getData(), op), minData: results.weight1.minBuffer.getData(), maxData: results.weight1.maxBuffer.getData() }; - results = aggregator.run( - Object.assign({}, fixture, {useGPU: true}, pointsData, {weights: {weight1: maxMinweight}}) - ); + results = gpuAggregator.run(Object.assign(aggregationOpts, {weights: {weight1: maxMinweight}})); gpuResults.maxMinData = results.weight1.maxMinBuffer.getData(); // Compare aggregation details for each grid-cell, total count and max count. - verifyResults({t, cpuResults, gpuResults, testName}); + verifyResults({t, cpuResults, gpuResults, testName: aggregation}); config.EPSILON = oldEpsilon; } test('GPUGridAggregator#CompareCPUandGPU', t => { - const pointsData = generateRandomGridPoints(5000); - for (const opName in AGGREGATION_OPERATION) { - testAggregationOperations({t, testName: opName, op: AGGREGATION_OPERATION[opName], pointsData}); - } - t.end(); -}); - -test('GPUGridAggregator worldspace aggregation #CompareCPUandGPU', t => { - Object.assign(fixtureWorldSpace, GridAggregationData.buildAttributes(fixtureWorldSpace)); - - const sa = new GPUGridAggregator(gl); - let results = sa.run(Object.assign({}, fixtureWorldSpace, {useGPU: false})); - const cpuResults = { - aggregationData: results.weight1.aggregationBuffer.getData(), - minData: results.weight1.minBuffer.getData(), - maxData: results.weight1.maxBuffer.getData() - }; - - // 32-bit aggregation - results = sa.run(Object.assign({}, fixtureWorldSpace, {useGPU: true})); - const gpuResults = { - aggregationData: results.weight1.aggregationBuffer.getData(), - minData: results.weight1.minBuffer.getData(), - maxData: results.weight1.maxBuffer.getData() + const randomData = generateRandomGridPoints(5000); + const {attributes, vertexCount, data} = buildAttributes(randomData); + const pointsData = { + data, + weights: randomData.weights, + attributes, + vertexCount }; - t.deepEqual(gpuResults, cpuResults, '32bit aggregation: cpu and gpu results should match'); - - // 64-bit aggregation - // TODO: enable after integrating fp64 extension - // results = sa.run(Object.assign({}, fixtureWorldSpace, {useGPU: true, fp64: true, attributes, vertexCount})); - // gpuResults = { - // aggregationData: results.weight1.aggregationBuffer.getData(), - // minData: results.weight1.minBuffer.getData(), - // maxData: results.weight1.maxBuffer.getData() - // }; - // t.deepEqual(gpuResults, cpuResults, '64bit aggregation: cpu and gpu results should match'); - - t.end(); -}); - -test('GPUGridAggregator#ChangeFlags#dataChanged', t => { - const aggregator = new GPUGridAggregator(gl); - - let useGPU = false; - let results = aggregator.run(Object.assign({}, fixture, {useGPU})); - const cpuResults = getCPUResults(results.weight1); - - // Change only data (positions and weights) - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - positions: fixtureUpdated.positions, - weights: fixtureUpdated.weights, - changeFlags: {dataChanged: true} - }) - ); - - const cpuResultsUpdated = getCPUResults(results.weight1); - - useGPU = true; - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - changeFlags: {} // switch from cpu to gpu should internally should treat as dataChanged=true - }) - ); - const gpuResults = getGPUResults(results.weight1); - - // Change only data (positions and weights) - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - positions: fixtureUpdated.positions, - weights: fixtureUpdated.weights, - changeFlags: {dataChanged: true} - }) - ); - - const gpuResultsUpdated = getGPUResults(results.weight1); - - t.deepEqual(gpuResults, cpuResults, 'cpu and gpu results should match'); - t.deepEqual(gpuResultsUpdated, cpuResultsUpdated, 'cpu and gpu results should match'); - t.end(); -}); - -test('GPUGridAggregator#ChangeFlags#cellSizeChanged', t => { - const aggregator = new GPUGridAggregator(gl); - - let useGPU = false; - let results = aggregator.run(Object.assign({}, fixture, {useGPU})); - const cpuResults = getCPUResults(results.weight1); - - // Change only cellSize - const biggerCellSize = fixture.cellSize.map(x => x * 15); - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - positions: null, - weights: null, - cellSize: biggerCellSize, - changeFlags: {cellSizeChanged: true} - }) - ); - - const cpuResultsUpdated = getCPUResults(results.weight1); - - useGPU = true; - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - changeFlags: {} // switch from cpu to gpu should internally treated as dataChanged=true - }) - ); - const gpuResults = getGPUResults(results.weight1); - - // Change only data (positions and weights) - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - positions: null, - weights: null, - cellSize: biggerCellSize, - changeFlags: {cellSizeChanged: true} - }) - ); - - const gpuResultsUpdated = getGPUResults(results.weight1); - - t.deepEqual(gpuResults, cpuResults, 'cpu and gpu results should match'); - t.deepEqual(gpuResultsUpdated, cpuResultsUpdated, 'cpu and gpu results should match'); - t.end(); -}); - -test('GPUGridAggregator#ChangeFlags#viewportChanged', t => { - const aggregator = new GPUGridAggregator(gl); - - let useGPU = false; - let results = aggregator.run(Object.assign({}, fixture, {useGPU})); - const cpuResults = getCPUResults(results.weight1); - - // Change only viewport - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - viewport: fixtureUpdated.viewport, - changeFlags: {viewportChanged: true} - }) - ); - - const cpuResultsUpdated = getCPUResults(results.weight1); - - useGPU = true; - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - changeFlags: {} // switch from cpu to gpu should internally treated as dataChanged=true - }) - ); - const gpuResults = getGPUResults(results.weight1); - - // Change only data (positions and weights) - results = aggregator.run( - Object.assign({}, fixture, { - useGPU, - viewport: fixtureUpdated.viewport, - changeFlags: {viewportChanged: true} - }) - ); - - const gpuResultsUpdated = getGPUResults(results.weight1); - - t.deepEqual(gpuResults, cpuResults, 'cpu and gpu results should match'); - t.deepEqual(gpuResultsUpdated, cpuResultsUpdated, 'cpu and gpu results should match'); - t.end(); -}); - -test('GPUGridAggregator#getData', t => { - const aggregator = new GPUGridAggregator(gl); - const weight1 = Object.assign({}, fixture.weights.weight1, {size: 3}); - - // Run on GPU - aggregator.run(Object.assign({}, fixture, {weights: {weight1}, useGPU: true})); - const gpuResults = aggregator.getData('weight1'); - - // Run on CPU - aggregator.run(Object.assign({}, fixture, {weights: {weight1}, useGPU: false})); - const cpuResults = aggregator.getData('weight1'); - - t.deepEqual(gpuResults, cpuResults, 'cpu and gpu results should match'); + for (const aggregation in AGGREGATION_OPERATION) { + testAggregationOperations({t, aggregation, op: AGGREGATION_OPERATION[aggregation], pointsData}); + } t.end(); }); diff --git a/test/modules/aggregation-layers/utils/grid-aggregation-utils.spec.js b/test/modules/aggregation-layers/utils/grid-aggregation-utils.spec.js index fef80244f37..e3cdb0b2b13 100644 --- a/test/modules/aggregation-layers/utils/grid-aggregation-utils.spec.js +++ b/test/modules/aggregation-layers/utils/grid-aggregation-utils.spec.js @@ -20,28 +20,7 @@ import test from 'tape-catch'; -import {alignToCell} from '@deck.gl/aggregation-layers/utils/gpu-grid-aggregation/grid-aggregation-utils'; - -import GPUGridAggregator from '@deck.gl/aggregation-layers/utils/gpu-grid-aggregation/gpu-grid-aggregator'; -import {pointToDensityGridData} from '@deck.gl/aggregation-layers/utils/gpu-grid-aggregation/grid-aggregation-utils'; - -import {gl} from '@deck.gl/test-utils'; -import {points, GridAggregationData} from 'deck.gl-test/data'; - -const getPosition = d => d.COORDINATES; -const gpuGridAggregator = new GPUGridAggregator(gl); - -function filterEmptyChannels(inArray) { - const outArray = []; - for (let i = 0; i < inArray.length; i += 4) { - outArray.push(inArray[i], inArray[i + 3]); - } - return outArray; -} - -function compareArrays(t, name, cpu, gpu) { - t.deepEqual(filterEmptyChannels(gpu), filterEmptyChannels(cpu), name); -} +import {alignToCell} from '@deck.gl/aggregation-layers/utils/grid-aggregation-utils'; test('GridAggregationUtils#alignToCell (CPU)', t => { t.equal(alignToCell(-3, 5), -5); @@ -49,50 +28,3 @@ test('GridAggregationUtils#alignToCell (CPU)', t => { t.end(); }); - -test('GridAggregationUtils#pointToDensityGridData (CPU vs GPU)', t => { - const opts = { - data: points, - getPosition, - weightParams: {weight: {needMax: 1, needMin: 1, getWeight: x => 1}}, - gpuGridAggregator, - aggregationFlags: {dataChanged: true}, - fp64: false // TODO: enable once FP64 extension support is resolved - }; - const {attributes, vertexCount} = GridAggregationData.buildAttributes({ - data: opts.data, - weights: opts.weightParams, - getPosition: x => x.COORDINATES - }); - const CELLSIZES = [1000, 5000]; // cell size 500 requires 64 bit aggregation - for (const cellSizeMeters of CELLSIZES) { - opts.cellSizeMeters = cellSizeMeters; - opts.gpuAggregation = false; - const cpuResults = pointToDensityGridData(Object.assign({}, opts, {attributes, vertexCount})); - opts.gpuAggregation = true; - const gpuResults = pointToDensityGridData(Object.assign({}, opts, {attributes, vertexCount})); - - compareArrays( - t, - `Cell aggregation data should match for cellSizeMeters:${cellSizeMeters}`, - cpuResults.weights.weight.aggregationBuffer.getData(), - gpuResults.weights.weight.aggregationBuffer.getData() - ); - - compareArrays( - t, - `Max data should match for cellSizeMeters:${cellSizeMeters}`, - cpuResults.weights.weight.maxBuffer.getData(), - gpuResults.weights.weight.maxBuffer.getData() - ); - - compareArrays( - t, - `Min data should match for cellSizeMeters:${cellSizeMeters}`, - cpuResults.weights.weight.minBuffer.getData(), - gpuResults.weights.weight.minBuffer.getData() - ); - } - - t.end(); -}); diff --git a/test/render/test-cases.js b/test/render/test-cases.js index afde80fcb4e..b6fc6f7d755 100644 --- a/test/render/test-cases.js +++ b/test/render/test-cases.js @@ -1419,19 +1419,6 @@ export const TEST_CASES = [ ], goldenImage: './test/render/golden-images/gpu-grid-lnglat.png' }, - { - name: 'gpu-grid-lnglat-cpu-aggregation', - viewState: GRID_LAYER_INFO.viewState, - layers: [ - new GPUGridLayer( - Object.assign({}, GRID_LAYER_INFO.props, { - id: 'gpu-grid-lnglat-cpu-aggregation', - gpuAggregation: false - }) - ) - ], - goldenImage: './test/render/golden-images/gpu-grid-lnglat.png' - }, { name: 'contour-lnglat-cpu-aggregation', viewState: {