Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ImageBitmap and OffscreenCanvas for DEM tiles #8845

Merged
merged 18 commits into from
Dec 10, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions bench/benchmarks/hillshade_load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// @flow

import Benchmark from '../lib/benchmark';
import createMap from '../lib/create_map';
import type {StyleSpecification} from '../../src/style-spec/types';

export default class HillshadeLoad extends Benchmark {
style: StyleSpecification;

constructor(style: string) {
super();
this.style = {
"version": 8,
"name": "Hillshade-only",
"center": [-112.81596278901452, 37.251160384573595],
"zoom": 11.560975632435424,
"bearing": 0,
"pitch": 0,
"sources": {
"mapbox://mapbox.terrain-rgb": {
"url": "mapbox://mapbox.terrain-rgb",
"type": "raster-dem",
"tileSize": 256
}
},
"layers": [
{
"id": "mapbox-terrain-rgb",
"type": "hillshade",
"source": "mapbox://mapbox.terrain-rgb",
"layout": {},
"paint": {}
}
]
};
}

bench() {
return createMap({
width: 1024,
height: 1024,
style: this.style,
stubRender: false,
showMap: true
}).then((map) => {
return new Promise((resolve, reject) => {
map.once('idle', () => {
resolve()
map.remove();
});
});
});
}
}
25 changes: 17 additions & 8 deletions bench/lib/create_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import Map from '../../src/ui/map';

export default function (options: any): Promise<Map> {
return new Promise((resolve, reject) => {
if(options){
options.stubRender = options.stubRender == null ? true: options.stubRender;
options.showMap = options.showMap == null ? false: options.showMap;
}

const container = document.createElement('div');
container.style.width = `${options.width || 512}px`;
container.style.height = `${options.width || 512}px`;
container.style.margin = '0 auto';
container.style.display = 'none';

if(!options.showMap){
container.style.display = 'none';
}
(document.body: any).appendChild(container);

const map = new Map(Object.assign({
Expand All @@ -18,15 +26,16 @@ export default function (options: any): Promise<Map> {

map
.on('load', () => {
// Stub out `_rerender`; benchmarks need to be the only trigger of `_render` from here on out.
map._rerender = () => {};
if(options.stubRender){
// Stub out `_rerender`; benchmarks need to be the only trigger of `_render` from here on out.
map._rerender = () => {};

// If there's a pending rerender, cancel it.
if (map._frame) {
map._frame.cancel();
map._frame = null;
// If there's a pending rerender, cancel it.
if (map._frame) {
map._frame.cancel();
map._frame = null;
}
}

resolve(map);
})
.on('error', (e) => reject(e.error))
Expand Down
2 changes: 2 additions & 0 deletions bench/versions/benchmarks.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import PaintStates from '../benchmarks/paint_states';
import {PropertyLevelRemove, FeatureLevelRemove, SourceLevelRemove} from '../benchmarks/remove_paint_state';
import {LayerBackground, LayerCircle, LayerFill, LayerFillExtrusion, LayerHeatmap, LayerHillshade, LayerLine, LayerRaster, LayerSymbol, LayerSymbolWithIcons} from '../benchmarks/layers';
import Load from '../benchmarks/map_load';
import HillshadeLoad from '../benchmarks/hillshade_load';
import Validate from '../benchmarks/style_validate';
import StyleLayerCreate from '../benchmarks/style_layer_create';
import QueryPoint from '../benchmarks/query_point';
Expand Down Expand Up @@ -69,6 +70,7 @@ register('LayoutDDS', new LayoutDDS());
register('SymbolLayout', new SymbolLayout(style, styleLocations.map(location => location.tileID[0])));
register('FilterCreate', new FilterCreate());
register('FilterEvaluate', new FilterEvaluate());
register('HillshadeLoad', new HillshadeLoad());

Promise.resolve().then(() => {
// Ensure the global worker pool is never drained. Browsers have resource limits
Expand Down
9 changes: 9 additions & 0 deletions flow-typed/offscreen-canvas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @flow strict

declare class OffscreenCanvas {
width: number;
height: number;

constructor(width: number, height: number): OffscreenCanvas;
getContext(contextType: '2d' ): CanvasRenderingContext2D;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: newline

2 changes: 1 addition & 1 deletion src/source/image_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class ImageSource extends Evented implements Source {
dispatcher: Dispatcher;
map: Map;
texture: Texture | null;
image: HTMLImageElement;
image: HTMLImageElement | ImageBitmap;
tileID: CanonicalTileID;
_boundsArray: RasterBoundsArray;
boundsBuffer: VertexBuffer;
Expand Down
5 changes: 4 additions & 1 deletion src/source/raster_dem_tile_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {getImage, ResourceType} from '../util/ajax';
import {extend} from '../util/util';
import {Evented} from '../util/evented';
import browser from '../util/browser';
import window from '../util/window';
import offscreenCanvasSupported from '../util/offscreen_canvas_supported';
import {OverscaledTileID} from './tile_id';
import RasterTileSource from './raster_tile_source';
// ensure DEMData is registered for worker transfer on main thread:
Expand Down Expand Up @@ -54,7 +56,8 @@ 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, 1);
const transfer = window.ImageBitmap && img instanceof window.ImageBitmap && offscreenCanvasSupported();
const rawImageData = transfer ? img : browser.getImageData(img, 1);
const params = {
uid: tile.uid,
coord: tile.tileID,
Expand Down
23 changes: 22 additions & 1 deletion src/source/raster_dem_tile_worker_source.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
// @flow

import DEMData from '../data/dem_data';
import {RGBAImage} from '../util/image';
import window from '../util/window';

import type Actor from '../util/actor';
import type {
WorkerDEMTileParameters,
WorkerDEMTileCallback,
TileParameters
} from './worker_source';
const {ImageBitmap} = window;

class RasterDEMTileWorkerSource {
actor: Actor;
loaded: {[string]: DEMData};
offcreenCanvas: OffscreenCanvas;
offcreenCanvasContext: CanvasRenderingContext2D;

constructor() {
this.loaded = {};
}

loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) {
const {uid, encoding, rawImageData} = params;
const dem = new DEMData(uid, rawImageData, encoding);

const imagePixels = (ImageBitmap && rawImageData instanceof ImageBitmap) ? this.getImageData(rawImageData) : rawImageData;
const dem = new DEMData(uid, imagePixels, encoding);
this.loaded = this.loaded || {};
this.loaded[uid] = dem;
callback(null, dem);
}

getImageData(imgBitmap: ImageBitmap): RGBAImage {
// Lazily initialize OffscreenCanvas
if (!this.offcreenCanvas || !this.offcreenCanvasContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo in variable name

this.offcreenCanvas = new OffscreenCanvas(512, 512);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does 512×512 suffice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

256x256 is probably better I had this initially because thats what i though our tile resolution was.

this.offcreenCanvasContext = this.offcreenCanvas.getContext('2d');
}

this.offcreenCanvas.width = imgBitmap.width;
this.offcreenCanvas.height = imgBitmap.height;
this.offcreenCanvasContext.drawImage(imgBitmap, 0, 0, imgBitmap.width, imgBitmap.height);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reusing the OffscreenCanvas for decoding images that are smaller, the right/bottom backfill border might contain leftovers from the previously decoded image.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good catch! Didn't think about this!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a clearRect call and re-ran the benchmarks, doesn't seem to have a performance impact.

// Insert an additional 1px padding around the image to allow backfilling for neighboring data.
const imgData = this.offcreenCanvasContext.getImageData(-1, -1, imgBitmap.width + 2, imgBitmap.height + 2);
return new RGBAImage({width: imgData.width, height: imgData.height}, imgData.data);
}

removeTile(params: TileParameters) {
const loaded = this.loaded,
uid = params.uid;
Expand Down
4 changes: 3 additions & 1 deletion src/source/worker_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type DEMData from '../data/dem_data';
import type {StyleGlyph} from '../style/style_glyph';
import type {StyleImage} from '../style/style_image';
import type {PromoteIdSpecification} from '../style-spec/types';
import window from '../util/window';
const {ImageBitmap} = window;

export type TileParameters = {
source: string,
Expand All @@ -33,7 +35,7 @@ export type WorkerTileParameters = TileParameters & {

export type WorkerDEMTileParameters = TileParameters & {
coord: { z: number, x: number, y: number, w: number },
rawImageData: RGBAImage,
rawImageData: RGBAImage | ImageBitmap,
encoding: "mapbox" | "terrarium"
};

Expand Down
42 changes: 31 additions & 11 deletions src/util/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import config from './config';
import assert from 'assert';
import {cacheGet, cachePut} from './tile_request_cache';
import webpSupported from './webp_supported';
import offscreenCanvasSupported from './offscreen_canvas_supported';

import type {Callback} from '../types/callback';
import type {Cancelable} from '../types/cancelable';
Expand Down Expand Up @@ -257,14 +258,38 @@ function sameOrigin(url) {

const transparentPngUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=';

function arrayBufferToImage(data: ArrayBuffer, callback: (err: ?Error, image: ?HTMLImageElement) => void, cacheControl: ?string, expires: ?string) {
const img: HTMLImageElement = new window.Image();
const URL = window.URL || window.webkitURL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the || window.webkitURL here?

img.onload = () => {
callback(null, img);
URL.revokeObjectURL(img.src);
};
img.onerror = () => callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.'));
const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'});
(img: any).cacheControl = cacheControl;
(img: any).expires = expires;
img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl;
}

function arrayBufferToImageBitmap(data: ArrayBuffer, callback: (err: ?Error, image: ?ImageBitmap) => void) {
const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'});
window.createImageBitmap(blob).then((imgBitmap) => {
callback(null, imgBitmap);
}).catch((e) => {
console.log(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to leave this in?

callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.'));
});
}

let imageQueue, numImageRequests;
export const resetImageRequestQueue = () => {
imageQueue = [];
numImageRequests = 0;
};
resetImageRequestQueue();

export const getImage = function(requestParameters: RequestParameters, callback: Callback<HTMLImageElement>): Cancelable {
export const getImage = function(requestParameters: RequestParameters, callback: Callback<HTMLImageElement | ImageBitmap>): Cancelable {
if (webpSupported.supported) {
if (!requestParameters.headers) {
requestParameters.headers = {};
Expand Down Expand Up @@ -309,16 +334,11 @@ export const getImage = function(requestParameters: RequestParameters, callback:
if (err) {
callback(err);
} else if (data) {
const img: HTMLImageElement = new window.Image();
img.onload = () => {
callback(null, img);
window.URL.revokeObjectURL(img.src);
};
img.onerror = () => callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.'));
const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'});
(img: any).cacheControl = cacheControl;
(img: any).expires = expires;
img.src = data.byteLength ? window.URL.createObjectURL(blob) : transparentPngUrl;
if (offscreenCanvasSupported()) {
arrayBufferToImageBitmap(data, callback);
} else {
arrayBufferToImage(data, callback, cacheControl, expires);
}
}
});

Expand Down
14 changes: 14 additions & 0 deletions src/util/offscreen_canvas_supported.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @flow
import window from './window';

let supportsOffscreenCanvas = null;

export default function offscreenCanvasSupported(): boolean {
if (supportsOffscreenCanvas == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ===

Copy link
Contributor Author

@arindam1993 arindam1993 Dec 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional as a null or undefined check

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then let's use

Suggested change
if (supportsOffscreenCanvas == null) {
if (!supportsOffscreenCanvas) {

Copy link
Contributor Author

@arindam1993 arindam1993 Dec 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then that would run when supportsOffscreenCanvas is false as well. I was intending this to be lazy and cached.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what situations would supportsOffscreenCanvas be undefined then? Let's add some flowtype to the variable to make it clearer what states it can be in.

Copy link
Contributor Author

@arindam1993 arindam1993 Dec 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really wound't ever be undefined I was just sticking to the most easiest maybe type refinement implementation for flow( I guess i still forgot the annotation 🤦‍♂ ) https://flow.org/en/docs/types/maybe/#toc-refining-maybe-types (the 2nd example)

I added the ?boolean annotation and got rid of the initial null assignment so its more inline with flow maybe types.

supportsOffscreenCanvas = window.OffscreenCanvas &&
new window.OffscreenCanvas(1, 1).getContext('2d') &&
typeof window.createImageBitmap === 'function';
}

return supportsOffscreenCanvas;
}
10 changes: 8 additions & 2 deletions src/util/web_worker_transfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import CompoundExpression from '../style-spec/expression/compound_expression';
import expressions from '../style-spec/expression/definitions';
import ResolvedImage from '../style-spec/expression/types/resolved_image';
import window from './window';
const {ImageData} = window;
const {ImageData, ImageBitmap} = window;

import type {Transferable} from '../types/transferable';

Expand Down Expand Up @@ -105,6 +105,11 @@ function isArrayBuffer(val: any): boolean {
(val instanceof ArrayBuffer || (val.constructor && val.constructor.name === 'ArrayBuffer'));
}

function isImageBitmap(val: any): boolean {
return ImageBitmap &&
val instanceof ImageBitmap;
}

/**
* Serialize the given object for transfer to or from a web worker.
*
Expand Down Expand Up @@ -133,7 +138,7 @@ export function serialize(input: mixed, transferables: ?Array<Transferable>): Se
return input;
}

if (isArrayBuffer(input)) {
if (isArrayBuffer(input) || isImageBitmap(input)) {
if (transferables) {
transferables.push(((input: any): ArrayBuffer));
}
Expand Down Expand Up @@ -224,6 +229,7 @@ export function deserialize(input: Serialized): mixed {
input instanceof Date ||
input instanceof RegExp ||
isArrayBuffer(input) ||
isImageBitmap(input) ||
ArrayBuffer.isView(input) ||
input instanceof ImageData) {
return input;
Expand Down