diff --git a/modules/gpu-table/package.json b/modules/gpu-table/package.json index 3cfe76a43fa..2390191a7f2 100644 --- a/modules/gpu-table/package.json +++ b/modules/gpu-table/package.json @@ -31,5 +31,9 @@ "dependencies": { "@loaders.gl/core": "^1.3.3", "@loaders.gl/arrow": "^1.3.3" + }, + "peerDependencies": { + "@deck.gl/core": "^7.3.3", + "@luma.gl/core": "^7.3.2" } } diff --git a/modules/gpu-table/src/lib/gpu-table.js b/modules/gpu-table/src/lib/gpu-table.js index 67f4ed19b00..f26e537ff19 100644 --- a/modules/gpu-table/src/lib/gpu-table.js +++ b/modules/gpu-table/src/lib/gpu-table.js @@ -18,6 +18,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import {Buffer, _Accessor as Accessor} from '@luma.gl/core'; +import {log} from '@deck.gl/core'; + /* eslint-disable guard-for-in */ import GPUColumn from './gpu-column'; // import log from '../utils/log'; @@ -28,7 +31,8 @@ export default class GPUTable { // } } - constructor(gl, {id = 'gpu-table', stats} = {}) { + constructor(gl, opts = {}) { + const {id = 'gpu-table', stats} = opts; this.id = id; this.gl = gl; @@ -37,11 +41,15 @@ export default class GPUTable { this.updateTriggers = {}; this.accessors = {}; + this.buffers = {}; this.needsRedraw = true; this.userData = {}; this.stats = stats; + if ('columns' in opts) { + this.addColumns(opts.columns); + } // For sanity, prevent later addition of new members Object.seal(this); } @@ -128,6 +136,34 @@ export default class GPUTable { throw new Error('not implemented'); } + // Takes an object where key is the name of the colum and value is an object {data, type, size, ..} + addColumns(columns) { + for (const name in columns) { + log.assert(!this.buffers[name]); + const {accessor, buffer, data} = columns[name]; + log.assert(buffer || data); + this.accessors[name] = accessor || Accessor.resolve(columns[name]); + this.buffers[name] = + buffer || + new Buffer(this.gl, { + data: columns[name].data, + accessor: this.accessors[name] + }); + } + } + + // takes one ora array of column names to be deleted + removeColumns(name) { + const names = Array.isArray(name) ? name : [name]; + for (const n of names) { + if (n in this.buffers) { + this.buffers[n].delete(); + delete this.buffers[n]; + delete this.accessors[n]; + } + } + } + // PRIVATE METHODS _createAttribute(name, attribute, extraProps) { diff --git a/modules/gpu-table/src/lib/gpu-transform.js b/modules/gpu-table/src/lib/gpu-transform.js new file mode 100644 index 00000000000..516b65a718f --- /dev/null +++ b/modules/gpu-table/src/lib/gpu-transform.js @@ -0,0 +1,116 @@ +// 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. + +// should create a new column based on mapping and gpuTable. +import {Buffer, Transform, _Accessor as Accessor} from '@luma.gl/core'; +import {log} from '@deck.gl/core'; +import GL from '@luma.gl/constants'; + +export default function gpuTransform(gpuTable, mappings) { + // TODO: For now hardcoding everything for position + const longitudeBuffer = gpuTable.buffers.longitude; + const latitudeBuffer = gpuTable.buffers.latitude; + + const byteLength = longitudeBuffer.byteLength * 3; + const positionAccessor = Accessor.resolve({size: 3, type: GL.FLOAT}); + const positionBuffer = new Buffer(gpuTable.gl, {byteLength, accessor: positionAccessor}); + const targetAccessors = {position: positionAccessor}; + + const {inject, varyings} = getShaderOptions(gpuTable, mappings, targetAccessors); + const transform = new Transform(gpuTable.gl, { + sourceBuffers: { + longitude: longitudeBuffer, + latitude: latitudeBuffer + }, + feedbackBuffers: { + position: positionBuffer + }, + varyings, + inject, + elementCount: longitudeBuffer.getElementCount(), + vs: 'void main() {}' + }); + transform.run(); + transform.delete(); + gpuTable.addColumns({position: {buffer: positionBuffer, accessor: positionAccessor}}); + return; +} + +function getShaderOptions(gpuTable, mappings, targetAccessors) { + let columns = []; + let glslCode = ''; + let declarations = ''; + const varyings = []; + for (const target in mappings) { + if (mappings[target].sourceColumns) { + columns = columns.concat(mappings[target].sourceColumns); + } + glslCode = `${glslCode}${mappings[target].glslCode}\n`; + const accessor = targetAccessors[target]; + const type = getType(accessor); + declarations = `${declarations}varying ${type} ${target};\n`; + varyings.push(target); + } + + // filter out duplicates + columns = new Set(columns); + columns.forEach(column => { + const accessor = gpuTable.accessors[column]; + const type = getType(accessor); + declarations = `${declarations}attribute ${type} ${column};\n`; + }); + return { + inject: { + 'vs:#decl': declarations, + 'vs:#main-start': glslCode + }, + varyings + }; +} + +// TODO +// Utility methods that can be moved to some global scope +// or just any existing luma.gl shadertools methods + +function getType(accessor) { + switch (accessor.type) { + case GL.FLOAT: + switch (accessor.size) { + case 1: + return 'float'; + case 2: + return 'vec2'; + case 3: + return 'vec3'; + case 4: + return 'vec4'; + default: + // invalid size + log.assert(false); + break; + } + break; + default: + // TODO: add other cases + log.assert(false); + break; + } + return null; +} diff --git a/modules/gpu-table/test/index.js b/modules/gpu-table/test/index.js index 8db80d1d6a7..80b39fe7b12 100644 --- a/modules/gpu-table/test/index.js +++ b/modules/gpu-table/test/index.js @@ -1,3 +1,4 @@ import './lib/utils/typed-array-manager.spec'; import './lib/utils/double-precision.spec'; import './lib/gpu-table.spec'; +import './lib/gpu-transform.spec'; diff --git a/modules/gpu-table/test/lib/gpu-table.spec.js b/modules/gpu-table/test/lib/gpu-table.spec.js index 9b8045c6a5b..9733bfae3f9 100644 --- a/modules/gpu-table/test/lib/gpu-table.spec.js +++ b/modules/gpu-table/test/lib/gpu-table.spec.js @@ -1,7 +1,77 @@ import test from 'tape-catch'; import {GPUTable} from '@deck.gl/gpu-table'; +import {isWebGL2} from '@luma.gl/core'; +import {gl} from '@deck.gl/test-utils'; test('GPUTable#imports', t => { t.equals(typeof GPUTable, 'function', 'GPUTable import successful'); t.end(); }); + +test('GPUTable#addColumns', t => { + const LONGITUDES = [1.0, 2.0, 3.0]; + const LATITUDES = [11.0, 12.0, 13.0]; + const RADIUS = [10, 20, 30]; + const gpuTable = new GPUTable(gl, { + columns: { + longitude: {data: new Float32Array(LONGITUDES)}, + latitude: {data: new Float32Array(LATITUDES)} + } + }); + gpuTable.addColumns({ + radius: {data: new Float32Array(RADIUS)} + }); + + t.ok(gpuTable.buffers.longitude, 'should create longitude Buffer'); + t.ok(gpuTable.buffers.latitude, 'should create latitude Buffer'); + t.ok(gpuTable.buffers.radius, 'should create radius Buffer'); + t.ok(gpuTable.accessors.longitude, 'should create longitude Accessor'); + t.ok(gpuTable.accessors.latitude, 'should create latitude Accessor'); + t.ok(gpuTable.accessors.radius, 'should create radius Accessor'); + + // Buffer.getData() is WebGL2 only + if (isWebGL2(gl)) { + t.deepEqual( + gpuTable.buffers.longitude.getData(), + LONGITUDES, + 'should setup correct data for longitude Buffer' + ); + t.deepEqual( + gpuTable.buffers.latitude.getData(), + LATITUDES, + 'should setup correct data for longitude Buffer' + ); + t.deepEqual( + gpuTable.buffers.radius.getData(), + RADIUS, + 'should setup correct data for longitude Buffer' + ); + } + t.end(); +}); + +test('GPUTable#removeColumns', t => { + const LONGITUDES = [1.0, 2.0, 3.0]; + const LATITUDES = [11.0, 12.0, 13.0]; + const RADIUS = [10, 20, 30]; + const gpuTable = new GPUTable(gl, { + columns: { + longitude: {data: new Float32Array(LONGITUDES)}, + latitude: {data: new Float32Array(LATITUDES)} + } + }); + gpuTable.addColumns({ + radius: {data: new Float32Array(RADIUS)} + }); + + gpuTable.removeColumns(['longitude', 'radius']); + + t.notOk(gpuTable.buffers.longitude, 'should have deleted longitude Buffer'); + t.ok(gpuTable.buffers.latitude, "shouldn't delete latitude Buffer"); + t.notOk(gpuTable.buffers.radius, 'should have deleted radius Buffer'); + + gpuTable.removeColumns('latitude'); + t.notOk(gpuTable.buffers.latitude, 'should have deleted latitude Buffer'); + + t.end(); +}); diff --git a/modules/gpu-table/test/lib/gpu-transform.spec.js b/modules/gpu-table/test/lib/gpu-transform.spec.js new file mode 100644 index 00000000000..bfb65fec041 --- /dev/null +++ b/modules/gpu-table/test/lib/gpu-transform.spec.js @@ -0,0 +1,68 @@ +// 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 {GPUTable} from '@deck.gl/gpu-table'; + +import gpuTransform from '../../src/lib/gpu-transform'; +// import gpuTransform from '@deck.gl/gpu-table/gpu-transform'; + +import {isWebGL2} from '@luma.gl/core'; +import {gl} from '@deck.gl/test-utils'; + +test('gpuTransform#imports', t => { + t.equals(typeof gpuTransform, 'function', 'GPUTable import successful'); + t.end(); +}); + +test('gpuTransform', t => { + if (!isWebGL2(gl)) { + t.comment('gpuTransform requires WebGL2, skipping tests'); + t.end(); + return; + } + + const LONGITUDES = [1.0, 2.0, 3.0]; + const LATITUDES = [11.0, 12.0, 13.0]; + const gpuTable = new GPUTable(gl, { + columns: { + longitude: {data: new Float32Array(LONGITUDES)}, + latitude: {data: new Float32Array(LATITUDES)} + } + }); + gpuTransform(gpuTable, { + position: { + sourceColumns: ['longitude', 'latitude'], + glslCode: 'position = vec3(longitude, latitude, 0.0);' + } + }); + + t.ok(gpuTable.buffers.position, 'should create position Buffer'); + t.ok(gpuTable.accessors.position, 'should create position Accessor'); + + const positions = gpuTable.buffers.position.getData(); + for (let i = 0; i < LONGITUDES.length; i++) { + t.equal(positions[i * 3], LONGITUDES[i], `should match longitude at index: ${i}`); + t.equal(positions[i * 3 + 1], LATITUDES[i], `should match latitude at index: ${i}`); + t.equal(positions[i * 3 + 2], 0, `should match z at index: ${i}`); + } + t.end(); +});