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

Glyph Atlas improvements #1923

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion js/data/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ SymbolBucket.prototype.anchorIsTooClose = function(text, repeatDistance, anchor)
};

SymbolBucket.prototype.placeFeatures = function(collisionTile, buffers, collisionDebug) {

// Calculate which labels can be shown and when they can be shown and
// create the bufers used for rendering.

Expand All @@ -319,6 +318,7 @@ SymbolBucket.prototype.placeFeatures = function(collisionTile, buffers, collisio
var maxScale = collisionTile.maxScale;

elementGroups.glyph['text-size'] = layout['text-size'];
elementGroups.glyph['text-font'] = layout['text-font'];
elementGroups.icon['icon-size'] = layout['icon-size'];

var textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line';
Expand Down
9 changes: 7 additions & 2 deletions js/render/draw_symbol.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,15 @@ function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf,
}

if (text) {
painter.glyphAtlas.updateTexture(gl);
var textfont = elementGroups['text-font'];
var fontstack = textfont && textfont.join(',');
var glyphAtlas = fontstack && painter.glyphSource.getGlyphAtlas(fontstack);
if (!glyphAtlas) return;

glyphAtlas.updateTexture(gl);
vertex = tile.buffers.glyphVertex;
elements = tile.buffers.glyphElement;
texsize = [painter.glyphAtlas.width / 4, painter.glyphAtlas.height / 4];
texsize = [glyphAtlas.width / 4, glyphAtlas.height / 4];
} else {
var mapMoving = painter.options.rotating || painter.options.zooming;
var iconScaled = fontScale !== 1 || browser.devicePixelRatio !== painter.spriteAtlas.pixelRatio || iconsNeedLinear;
Expand Down
2 changes: 1 addition & 1 deletion js/render/line_atlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module.exports = LineAtlas;

/**
* Much like a GlyphAtlas, a LineAtlas lets us reuse rendered dashed lines
* A LineAtlas lets us reuse rendered dashed lines
* by writing many of them to a texture and then fetching their positions
* using .getDash.
*
Expand Down
3 changes: 1 addition & 2 deletions js/render/painter.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,7 @@ Painter.prototype.render = function(style, options) {
this.spriteAtlas = style.spriteAtlas;
this.spriteAtlas.setSprite(style.sprite);

this.glyphAtlas = style.glyphAtlas;
this.glyphAtlas.bind(this.gl);
this.glyphSource = style.glyphSource;

this.frameHistory.record(this.transform.zoom);

Expand Down
1 change: 0 additions & 1 deletion js/source/geojson_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototy

_unloadTile: function(tile) {
tile.unloadVectorData(this.map.painter);
this.glyphAtlas.removeGlyphs(tile.uid);
this.dispatcher.send('remove tile', { uid: tile.uid, source: this.id }, null, tile.workerID);
},

Expand Down
1 change: 0 additions & 1 deletion js/source/vector_tile_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ VectorTileSource.prototype = util.inherit(Evented, {

_unloadTile: function(tile) {
tile.unloadVectorData(this.map.painter);
this.glyphAtlas.removeGlyphs(tile.uid);
this.dispatcher.send('remove tile', { uid: tile.uid, source: this.id }, null, tile.workerID);
},

Expand Down
4 changes: 1 addition & 3 deletions js/style/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ var styleBatch = require('./style_batch');
var StyleLayer = require('./style_layer');
var ImageSprite = require('./image_sprite');
var GlyphSource = require('../symbol/glyph_source');
var GlyphAtlas = require('../symbol/glyph_atlas');
var SpriteAtlas = require('../symbol/sprite_atlas');
var LineAtlas = require('../render/line_atlas');
var util = require('../util/util');
Expand All @@ -21,7 +20,6 @@ module.exports = Style;
function Style(stylesheet, animationLoop) {
this.animationLoop = animationLoop || new AnimationLoop();
this.dispatcher = new Dispatcher(Math.max(browser.hardwareConcurrency - 1, 1), this);
this.glyphAtlas = new GlyphAtlas(1024, 1024);
this.spriteAtlas = new SpriteAtlas(512, 512);
this.lineAtlas = new LineAtlas(256, 512);

Expand Down Expand Up @@ -65,7 +63,7 @@ function Style(stylesheet, animationLoop) {
this.sprite.on('load', this.fire.bind(this, 'change'));
}

this.glyphSource = new GlyphSource(stylesheet.glyphs, this.glyphAtlas);
this.glyphSource = new GlyphSource(stylesheet.glyphs);
this._resolve();
this.fire('load');
}.bind(this);
Expand Down
1 change: 0 additions & 1 deletion js/style/style_batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ styleBatch.prototype = {
source.id = id;
source.style = this._style;
source.dispatcher = this._style.dispatcher;
source.glyphAtlas = this._style.glyphAtlas;
source
.on('load', this._style._forwardSourceEvent)
.on('error', this._style._forwardSourceEvent)
Expand Down
143 changes: 82 additions & 61 deletions js/symbol/bin_pack.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,105 @@
'use strict';

module.exports = BinPack;

/**
* Simple Bin Packing
* Uses the Shelf Best Height Fit algorithm from
* http://clb.demon.fi/files/RectangleBinPack.pdf
* @private
*/
function BinPack(width, height) {
this.width = width;
this.height = height;
this.free = [{ x: 0, y: 0, w: width, h: height }];
this.shelves = [];
this.stats = {};
this.count = function(h) {
this.stats[h] = (this.stats[h] | 0) + 1;
};
}

/**
* Simple algorithm to recursively merge the newly released cell with its
* neighbor. This doesn't merge more than two cells at a time, and fails
* for complicated merges.
* @private
*/
BinPack.prototype.release = function(rect) {
for (var i = 0; i < this.free.length; i++) {
var free = this.free[i];
BinPack.prototype.allocate = function(reqWidth, reqHeight) {
var y = 0,
best = { shelf: -1, waste: Infinity },
shelf, waste;

if (free.y === rect.y && free.h === rect.h && free.x + free.w === rect.x) {
free.w += rect.w;
// find shelf
for (var i = 0; i < this.shelves.length; i++) {
shelf = this.shelves[i];
y += shelf.height;

} else if (free.x === rect.x && free.w === rect.w && free.y + free.h === rect.y) {
free.h += rect.h;
// exactly the right height with width to spare, pack it..
if (reqHeight === shelf.height && reqWidth <= shelf.free) {
this.count(reqHeight);
return shelf.alloc(reqWidth, reqHeight);
}
// not enough height or width, skip it..
if (reqHeight > shelf.height || reqWidth > shelf.free) {
continue;
}
// maybe enough height or width, minimize waste..
if (reqHeight < shelf.height && reqWidth <= shelf.free) {
waste = shelf.height - reqHeight;
if (waste < best.waste) {
best.waste = waste;
best.shelf = i;
}
}
}

} else if (rect.y === free.y && rect.h === free.h && rect.x + rect.w === free.x) {
free.x = rect.x;
free.w += rect.w;
if (best.shelf !== -1) {
shelf = this.shelves[best.shelf];
this.count(reqHeight);
return shelf.alloc(reqWidth, reqHeight);
}

} else if (rect.x === free.x && rect.w === free.w && rect.y + rect.h === free.y) {
free.y = rect.y;
free.h += rect.h;
// add shelf
if (reqHeight <= (this.height - y) && reqWidth <= this.width) {
shelf = new Shelf(y, this.width, reqHeight);
this.shelves.push(shelf);
this.count(reqHeight);
return shelf.alloc(reqWidth, reqHeight);
}

} else continue;
// no more space
return {x: -1, y: -1};
};

this.free.splice(i, 1);
this.release(free);
return;

BinPack.prototype.resize = function(reqWidth, reqHeight) {
if (reqWidth < this.width || reqHeight < this.height) { return false; }
this.height = reqHeight;
this.width = reqWidth;
for (var i = 0; i < this.shelves.length; i++) {
this.shelves[i].resize(reqWidth);
}
this.free.push(rect);
return true;
};

BinPack.prototype.allocate = function(width, height) {
// Find the smallest free rect angle
var rect = { x: Infinity, y: Infinity, w: Infinity, h: Infinity };
var smallest = -1;
for (var i = 0; i < this.free.length; i++) {
var ref = this.free[i];
if (width <= ref.w && height <= ref.h && ref.y <= rect.y && ref.x <= rect.x) {
rect = ref;
smallest = i;
}
}

if (smallest < 0) {
// There's no space left for this char.
return { x: -1, y: -1 };
}
function Shelf(y, width, height) {
this.y = y;
this.x = 0;
this.width = this.free = width;
this.height = height;
}

this.free.splice(smallest, 1);
Shelf.prototype = {
alloc: function(reqWidth, reqHeight) {
if (reqWidth > this.free || reqHeight > this.height) {
return {x: -1, y: -1};
}
var x = this.x;
this.x += reqWidth;
this.free -= reqWidth;
return {x: x, y: this.y, w: reqWidth, h: reqHeight};
},

// Shorter/Longer Axis Split Rule (SAS)
// http://clb.demon.fi/files/RectangleBinPack.pdf p. 15
// Ignore the dimension of R and just split long the shorter dimension
// See Also: http://www.cs.princeton.edu/~chazelle/pubs/blbinpacking.pdf
if (rect.w < rect.h) {
// split horizontally
// +--+---+
// |__|___| <-- b1
// +------+ <-- b2
if (rect.w > width) this.free.push({ x: rect.x + width, y: rect.y, w: rect.w - width, h: height });
if (rect.h > height) this.free.push({ x: rect.x, y: rect.y + height, w: rect.w, h: rect.h - height });
} else {
// split vertically
// +--+---+
// |__| | <-- b1
// +--|---+ <-- b2
if (rect.w > width) this.free.push({ x: rect.x + width, y: rect.y, w: rect.w - width, h: rect.h });
if (rect.h > height) this.free.push({ x: rect.x, y: rect.y + height, w: width, h: rect.h - height });
resize: function(reqWidth) {
if (reqWidth < this.width) { return false; }
this.free += (reqWidth - this.width);
this.width = reqWidth;
return true;
}

return { x: rect.x, y: rect.y, w: width, h: height };
};

65 changes: 33 additions & 32 deletions js/symbol/glyph_atlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,38 +68,6 @@ GlyphAtlas.prototype.getRects = function() {
return rects;
};

GlyphAtlas.prototype.removeGlyphs = function(id) {
for (var key in this.ids) {

var ids = this.ids[key];

var pos = ids.indexOf(id);
if (pos >= 0) ids.splice(pos, 1);
this.ids[key] = ids;

if (!ids.length) {
var rect = this.index[key];

var target = this.data;
for (var y = 0; y < rect.h; y++) {
var y1 = this.width * (rect.y + y) + rect.x;
for (var x = 0; x < rect.w; x++) {
target[y1 + x] = 0;
}
}

this.dirty = true;

this.bin.release(rect);

delete this.index[key];
delete this.ids[key];
}
}


this.updateTexture(this.gl);
};

GlyphAtlas.prototype.addGlyph = function(id, name, glyph, buffer) {
if (!glyph) {
Expand Down Expand Up @@ -136,6 +104,10 @@ GlyphAtlas.prototype.addGlyph = function(id, name, glyph, buffer) {
packHeight += (4 - packHeight % 4);

var rect = this.bin.allocate(packWidth, packHeight);
if (rect.x < 0) {
this.resize();
rect = this.bin.allocate(packWidth, packHeight);
}
if (rect.x < 0) {
console.warn('glyph bitmap overflow');
return { glyph: glyph, rect: null };
Expand All @@ -159,6 +131,35 @@ GlyphAtlas.prototype.addGlyph = function(id, name, glyph, buffer) {
return rect;
};

GlyphAtlas.prototype.resize = function() {
var origw = this.width,
origh = this.height;

// For now, don't grow the atlas beyond 1024x1024 because of how
// texture coords pack into unsigned byte in symbol bucket.
if (origw > 512 || origh > 512) return;

if (this.texture) {
if (this.gl) {
this.gl.deleteTexture(this.texture);
}
this.texture = null;
}

this.width *= 2;
this.height *= 2;
this.bin.resize(this.width, this.height);

var buf = new ArrayBuffer(this.width * this.height),
src, dst;
for (var i = 0; i < origh; i++) {
src = new Uint8Array(this.data.buffer, origh * i, origw);
dst = new Uint8Array(buf, origh * i * 2, origw);
dst.set(src);
}
this.data = new Uint8Array(buf);
};

GlyphAtlas.prototype.bind = function(gl) {
this.gl = gl;
if (!this.texture) {
Expand Down
Loading