Skip to content

Commit

Permalink
DEMData: save one CPU copy and do decode on GPU
Browse files Browse the repository at this point in the history
Save one CPU copy and avoid decoding DEM data on CPU (do it on GPU).

Pad DEMData, for backfill, in context.getImageData. This avoids the need to pad it in DEMData constructor.

DEM decode to height is a single dot operation - no need to do it on CPU since we already have another decode (from internal format) in src/shaders/hillshade_prepare.fragment.glsl.
  • Loading branch information
astojilj committed Aug 27, 2019
1 parent 4eb7d4e commit 364419d
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 59 deletions.
51 changes: 24 additions & 27 deletions src/data/dem_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,56 +16,53 @@ import { register } from '../util/web_worker_transfer';

export default class DEMData {
uid: string;
data: Int32Array;
data: Uint32Array;
stride: number;
dim: number;
encoding: "mapbox" | "terrarium";

// RGBAImage data has uniform 1px padding on all sides: square tile edge size defines stride
// and dim is calculated as stride - 2.
constructor(uid: string, data: RGBAImage, encoding: "mapbox" | "terrarium") {
this.uid = uid;
if (data.height !== data.width) throw new RangeError('DEM tiles must be square');
if (encoding && encoding !== "mapbox" && encoding !== "terrarium") return warnOnce(
`"${encoding}" is not a valid encoding type. Valid types include "mapbox" and "terrarium".`
);
const dim = this.dim = data.height;
this.stride = this.dim + 2;
this.data = new Int32Array(this.stride * this.stride);

const pixels = data.data;
const unpack = encoding === "terrarium" ? this._unpackTerrarium : this._unpackMapbox;
for (let y = 0; y < dim; y++) {
for (let x = 0; x < dim; x++) {
const i = y * dim + x;
const j = i * 4;
this.set(x, y, unpack(pixels[j], pixels[j + 1], pixels[j + 2]));
}
}
this.stride = data.height;
const dim = this.dim = data.height - 2;
this.data = new Uint32Array(data.data.buffer);
this.encoding = encoding || 'mapbox';

// in order to avoid flashing seams between tiles, here we are initially populating a 1px border of pixels around the image
// with the data of the nearest pixel from the image. this data is eventually replaced when the tile's neighboring
// tiles are loaded and the accurate data can be backfilled using DEMData#backfillBorder
for (let x = 0; x < dim; x++) {
// left vertical border
this.set(-1, x, this.get(0, x));
this.data[this._idx(-1, x)] = this.data[this._idx(0, x)];
// right vertical border
this.set(dim, x, this.get(dim - 1, x));
this.data[this._idx(dim, x)] = this.data[this._idx(dim - 1, x)];
// left horizontal border
this.set(x, -1, this.get(x, 0));
this.data[this._idx(x, -1)] = this.data[this._idx(x, 0)];
// right horizontal border
this.set(x, dim, this.get(x, dim - 1));
this.data[this._idx(x, dim)] = this.data[this._idx(x, dim - 1)];
}
// corners
this.set(-1, -1, this.get(0, 0));
this.set(dim, -1, this.get(dim - 1, 0));
this.set(-1, dim, this.get(0, dim - 1));
this.set(dim, dim, this.get(dim - 1, dim - 1));
this.data[this._idx(-1, -1)] = this.data[this._idx(0, 0)];
this.data[this._idx(dim, -1)] = this.data[this._idx(dim - 1, 0)];
this.data[this._idx(-1, dim)] = this.data[this._idx(0, dim - 1)];
this.data[this._idx(dim, dim)] = this.data[this._idx(dim - 1, dim - 1)];
}

set(x: number, y: number, value: number) {
this.data[this._idx(x, y)] = value + 65536;
get(x: number, y: number) {
const pixels = new Uint8Array(this.data.buffer);
const index = this._idx(x, y) * 4;
const unpack = this.encoding === "terrarium" ? this._unpackTerrarium : this._unpackMapbox;
return unpack(pixels[index], pixels[index + 1], pixels[index + 2]);
}

get(x: number, y: number) {
return this.data[this._idx(x, y)] - 65536;
getUnpackVector() {
return this.encoding === "terrarium" ? [256.0, 1.0, 1.0 / 256.0, 32768.0] : [6553.6, 25.6, 0.1, 10000.0];
}

_idx(x: number, y: number) {
Expand Down Expand Up @@ -119,7 +116,7 @@ export default class DEMData {
const oy = -dy * this.dim;
for (let y = yMin; y < yMax; y++) {
for (let x = xMin; x < xMax; x++) {
this.set(x, y, borderTile.get(x + ox, y + oy));
this.data[this._idx(x, y)] = borderTile.data[this._idx(x + ox, y + oy)];
}
}
}
Expand Down
16 changes: 2 additions & 14 deletions src/render/draw_hillshade.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import type Painter from './painter';
import type SourceCache from '../source/source_cache';
import type HillshadeStyleLayer from '../style/style_layer/hillshade_style_layer';
import type DEMData from '../data/dem_data';
import type {OverscaledTileID} from '../source/tile_id';

export default drawHillshade;
Expand Down Expand Up @@ -68,26 +69,13 @@ function renderHillshade(painter, tile, layer, depthMode, stencilMode, colorMode
function prepareHillshade(painter, tile, layer, sourceMaxZoom, depthMode, stencilMode, colorMode) {
const context = painter.context;
const gl = context.gl;
// decode rgba levels by using integer overflow to convert each Uint32Array element -> 4 Uint8Array elements.
// ex.
// Uint32:
// base 10 - 67308
// base 2 - 0000 0000 0000 0001 0000 0110 1110 1100
//
// Uint8:
// base 10 - 0, 1, 6, 236 (this order is reversed in the resulting array via the overflow.
// first 8 bits represent 236, so the r component of the texture pixel will be 236 etc.)
// base 2 - 0000 0000, 0000 0001, 0000 0110, 1110 1100
if (tile.dem && tile.dem.data) {
const tileSize = tile.dem.dim;
const textureStride = tile.dem.stride;

const pixelData = tile.dem.getPixels();
context.activeTexture.set(gl.TEXTURE1);

// if UNPACK_PREMULTIPLY_ALPHA_WEBGL is set to true prior to drawHillshade being called
// tiles will appear blank, because as you can see above the alpha value for these textures
// is always 0
context.pixelStoreUnpackPremultiplyAlpha.set(false);
tile.demTexture = tile.demTexture || painter.getTileTexture(textureStride);
if (tile.demTexture) {
Expand Down Expand Up @@ -116,7 +104,7 @@ function prepareHillshade(painter, tile, layer, sourceMaxZoom, depthMode, stenci

painter.useProgram('hillshadePrepare').draw(context, gl.TRIANGLES,
depthMode, stencilMode, colorMode, CullFaceMode.disabled,
hillshadeUniformPrepareValues(tile, sourceMaxZoom),
hillshadeUniformPrepareValues(((tile: any): {dem: DEMData, tileID: OverscaledTileID}), sourceMaxZoom),
layer.id, painter.rasterBoundsBuffer,
painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments);

Expand Down
19 changes: 11 additions & 8 deletions src/render/program/hillshade_program.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// @flow

import assert from 'assert';
import { mat4 } from 'gl-matrix';

import {
Uniform1i,
Uniform1f,
Uniform2f,
UniformColor,
UniformMatrix4f
UniformMatrix4f,
Uniform4f
} from '../uniform_binding';
import EXTENT from '../../data/extent';
import MercatorCoordinate from '../../geo/mercator_coordinate';
Expand Down Expand Up @@ -36,7 +36,8 @@ export type HillshadePrepareUniformsType = {|
'u_image': Uniform1i,
'u_dimension': Uniform2f,
'u_zoom': Uniform1f,
'u_maxzoom': Uniform1f
'u_maxzoom': Uniform1f,
'u_unpack': Uniform4f
|};

const hillshadeUniforms = (context: Context, locations: UniformLocations): HillshadeUniformsType => ({
Expand All @@ -54,7 +55,8 @@ const hillshadePrepareUniforms = (context: Context, locations: UniformLocations)
'u_image': new Uniform1i(context, locations.u_image),
'u_dimension': new Uniform2f(context, locations.u_dimension),
'u_zoom': new Uniform1f(context, locations.u_zoom),
'u_maxzoom': new Uniform1f(context, locations.u_maxzoom)
'u_maxzoom': new Uniform1f(context, locations.u_maxzoom),
'u_unpack': new Uniform4f(context, locations.u_unpack)
});

const hillshadeUniformValues = (
Expand Down Expand Up @@ -84,10 +86,10 @@ const hillshadeUniformValues = (
};

const hillshadeUniformPrepareValues = (
tile: {dem: ?DEMData, tileID: OverscaledTileID}, maxzoom: number
tile: {dem: DEMData, tileID: OverscaledTileID}, maxzoom: number
): UniformValues<HillshadePrepareUniformsType> => {
assert(tile.dem);
const stride = ((tile.dem: any): DEMData).stride;

const stride = tile.dem.stride;
const matrix = mat4.create();
// Flip rendering at y axis.
mat4.ortho(matrix, 0, EXTENT, -EXTENT, 0, 0, 1);
Expand All @@ -98,7 +100,8 @@ const hillshadeUniformPrepareValues = (
'u_image': 1,
'u_dimension': [stride, stride],
'u_zoom': tile.tileID.overscaledZ,
'u_maxzoom': maxzoom
'u_maxzoom': maxzoom,
'u_unpack': tile.dem.getUnpackVector()
};
};

Expand Down
4 changes: 3 additions & 1 deletion src/shaders/hillshade_prepare.fragment.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ varying vec2 v_pos;
uniform vec2 u_dimension;
uniform float u_zoom;
uniform float u_maxzoom;
uniform vec4 u_unpack;

float getElevation(vec2 coord, float bias) {
// Convert encoded elevation value to meters
vec4 data = texture2D(u_image, coord) * 255.0;
return (data.r + data.g * 256.0 + data.b * 256.0 * 256.0) / 4.0;
data.a = -1.0;
return dot(data, u_unpack) / 4.0;
}

void main() {
Expand Down
3 changes: 1 addition & 2 deletions src/source/raster_dem_tile_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ class RasterDEMTileSource extends RasterTileSource implements Source {
if (this.map._refreshExpiredTiles) tile.setExpiryData(img);
delete (img: any).cacheControl;
delete (img: any).expires;

const rawImageData = browser.getImageData(img);
const rawImageData = browser.getImageData(img, 1);
const params = {
uid: tile.uid,
coord: tile.tileID,
Expand Down
4 changes: 2 additions & 2 deletions src/util/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const exported = {
return { cancel: () => cancel(frame) };
},

getImageData(img: CanvasImageSource): ImageData {
getImageData(img: CanvasImageSource, padding?: number = 0): ImageData {
const canvas = window.document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
Expand All @@ -45,7 +45,7 @@ const exported = {
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0, img.width, img.height);
return context.getImageData(0, 0, img.width, img.height);
return context.getImageData(-padding, -padding, img.width + 2 * padding, img.height + 2 * padding);
},

resolveURL(path: string) {
Expand Down
11 changes: 9 additions & 2 deletions test/ajax_stubs.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,15 @@ export const getImage = function({ url }, callback) {
});
};

browser.getImageData = function({width, height, data}) {
return {width, height, data: new Uint8Array(data)};
browser.getImageData = function({width, height, data}, padding = 0) {
const source = new Uint8Array(data);
const dest = new Uint8Array((2 * padding + width) * (2 * padding + height) * 4);

const offset = (2 * padding + width) * padding + padding;
for (let i = 0; i < height; i++) {
dest.set(source.slice(i * width * 4, (i + 1) * width * 4), 4 * (offset + (width + 2 * padding) * i));
}
return {width: width + 2 * padding, height: height + 2 * padding, data: dest};
};

// Hack: since node doesn't have any good video codec modules, just grab a png with
Expand Down
9 changes: 6 additions & 3 deletions test/unit/data/dem_data.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { RGBAImage } from '../../../src/util/image';
import { serialize, deserialize } from '../../../src/util/web_worker_transfer';

function createMockImage(height, width) {
// RGBAImage passed to constructor has uniform 1px padding on all sides.
height += 2;
width += 2;
const pixels = new Uint8Array(height * width * 4);
for (let i = 0; i < pixels.length; i++) {
pixels[i] = (i + 1) % 4 === 0 ? 1 : Math.floor(Math.random() * 256);
Expand All @@ -15,9 +18,8 @@ test('DEMData', (t) => {
t.test('constructor', (t) => {
const dem = new DEMData(0, {width: 4, height: 4, data: new Uint8ClampedArray(4 * 4 * 4)});
t.equal(dem.uid, 0);
t.equal(dem.dim, 4);
t.equal(dem.stride, 6);
t.true(dem.data instanceof Int32Array);
t.equal(dem.dim, 2);
t.equal(dem.stride, 4);
t.end();
});

Expand Down Expand Up @@ -139,6 +141,7 @@ test('DEMData#backfillBorder', (t) => {
dim: 4,
stride: 6,
data: dem0.data,
encoding: 'mapbox'
}, 'serializes DEM');

const transferrables = [];
Expand Down

0 comments on commit 364419d

Please sign in to comment.