Skip to content

Commit

Permalink
Switch from physical to logical segmentation of buffers
Browse files Browse the repository at this point in the history
Physical segmentation: each vertex buffer holds a maximum of 2¹⁶-1 vertices. When a layer requires more than that, create a new vertex buffer and index buffer.

Logical segmentation: no limit to the size of a vertex buffer. When reaching 2¹⁶-1 vertices, record the vertex and primitive offsets, then reset the indexing, starting a new "segment". Use the offsets when rendering each segment.

Logical segmentation is what native uses, and is generally simpler and more efficient.

While here, switch to indexed rendering for collision debug.
  • Loading branch information
jfirebaugh committed Oct 25, 2016
1 parent e03a7b1 commit 59f39b7
Show file tree
Hide file tree
Showing 23 changed files with 465 additions and 506 deletions.
80 changes: 65 additions & 15 deletions js/data/array_group.js
Expand Up @@ -2,39 +2,84 @@

const util = require('../util/util');

class Segment {
constructor(vertexOffset, primitiveOffset) {
this.vertexOffset = vertexOffset;
this.primitiveOffset = primitiveOffset;
this.vertexLength = 0;
this.primitiveLength = 0;
}
}

/**
* A class that manages vertex and element arrays for a range of features. It handles initialization,
* A class that manages vertex and element arrays for a bucket. It handles initialization,
* serialization for transfer to the main thread, and certain intervening mutations.
*
* Array elements are broken into array groups based on inherent limits of WebGL. Within a group is:
* A group has:
*
* * A "layout" vertex array, with fixed layout, containing values calculated from layout properties.
* * Zero, one, or two element arrays, with fixed layout, typically for eventual use in
* `gl.drawElements(gl.TRIANGLES, ...)`.
* * A "layout" vertex array, with fixed attributes, containing values calculated from layout properties.
* * Zero, one, or two element arrays, with fixed layout, for eventual `gl.drawElements` use.
* * Zero or more "paint" vertex arrays keyed by layer ID, each with a dynamic layout which depends
* on which paint properties of that layer use data-driven-functions (property functions or
* property-and-zoom functions). Values are calculated by evaluating those functions.
*
* Because indexed rendering is best done with 16 bit indices (and in fact, in WebGL, 16 bit
* indices are the only choice), a form of segmented addressing is used. Each group
* contains an `Array` of `Segment`s. A segment contains a vertex array offset, which forms
* the "base address" of indices within this segment. Each segment is drawn separately.
*
* @private
*/
class ArrayGroup {
constructor(arrayTypes) {
const LayoutVertexArrayType = arrayTypes.layoutVertexArrayType;
constructor(programInterface, programConfigurations) {
const LayoutVertexArrayType = programInterface.layoutVertexArrayType;
this.layoutVertexArray = new LayoutVertexArrayType();

const ElementArrayType = arrayTypes.elementArrayType;
const ElementArrayType = programInterface.elementArrayType;
if (ElementArrayType) this.elementArray = new ElementArrayType();

const ElementArrayType2 = arrayTypes.elementArrayType2;
const ElementArrayType2 = programInterface.elementArrayType2;
if (ElementArrayType2) this.elementArray2 = new ElementArrayType2();

this.paintVertexArrays = util.mapObject(arrayTypes.paintVertexArrayTypes, (PaintVertexArrayType) => {
return new PaintVertexArrayType();
this.paintVertexArrays = util.mapObject(programConfigurations, (programConfiguration) => {
const PaintVertexArrayType = programConfiguration.paintVertexArrayType();
const paintVertexArray = new PaintVertexArrayType();
paintVertexArray.programConfiguration = programConfiguration;
return paintVertexArray;
});

this.segments = [];
this.segments2 = [];
}

prepareSegment(numVertices) {
let segment = this.segments[this.segments.length - 1];
if (!segment || segment.vertexLength + numVertices > ArrayGroup.MAX_VERTEX_ARRAY_LENGTH) {
segment = new Segment(this.layoutVertexArray.length, this.elementArray.length);
this.segments.push(segment);
}
return segment;
}

prepareSegment2(numVertices) {
let segment = this.segments2[this.segments2.length - 1];
if (!segment || segment.vertexLength + numVertices > ArrayGroup.MAX_VERTEX_ARRAY_LENGTH) {
segment = new Segment(this.layoutVertexArray.length, this.elementArray2.length);
this.segments2.push(segment);
}
return segment;
}

hasCapacityFor(numVertices) {
return this.layoutVertexArray.length + numVertices <= ArrayGroup.MAX_VERTEX_ARRAY_LENGTH;
populatePaintArrays(layers, globalProperties, featureProperties) {
for (const layer of layers) {
const paintArray = this.paintVertexArrays[layer.id];
paintArray.programConfiguration.populatePaintArray(
layer,
paintArray,
this.layoutVertexArray.length,
globalProperties,
featureProperties);
}
}

isEmpty() {
Expand Down Expand Up @@ -63,8 +108,13 @@ class ArrayGroup {
elementArray: this.elementArray && this.elementArray.serialize(),
elementArray2: this.elementArray2 && this.elementArray2.serialize(),
paintVertexArrays: util.mapObject(this.paintVertexArrays, (array) => {
return array.serialize();
})
return {
array: array.serialize(),
type: array.constructor.serialize()
};
}),
segments: this.segments,
segments2: this.segments2
};
}

Expand Down
173 changes: 47 additions & 126 deletions js/data/bucket.js
Expand Up @@ -30,30 +30,20 @@ class Bucket {
constructor (options) {
this.zoom = options.zoom;
this.overscaling = options.overscaling;
this.layer = options.layer;
this.childLayers = options.childLayers;

this.layers = options.layers;
this.index = options.index;

this.programConfigurations = util.mapObject(this.programInterfaces, (programInterface) => {
const result = {};
for (const layer of options.childLayers) {
for (const layer of this.layers) {
result[layer.id] = ProgramConfiguration.createDynamic(programInterface.paintAttributes || [], layer, options);
}
return result;
});

if (options.arrays) {
this.bufferGroups = util.mapObject(options.arrays, (programArrayGroups, programName) => {
const programInterface = this.programInterfaces[programName];
const paintVertexArrayTypes = options.paintVertexArrayTypes[programName];
return programArrayGroups.map((arrayGroup) => {
return new BufferGroup(arrayGroup, {
layoutVertexArrayType: programInterface.layoutVertexArrayType.serialize(),
elementArrayType: programInterface.elementArrayType && programInterface.elementArrayType.serialize(),
elementArrayType2: programInterface.elementArrayType2 && programInterface.elementArrayType2.serialize(),
paintVertexArrayTypes: paintVertexArrayTypes
});
});
this.bufferGroups = util.mapObject(options.arrays, (arrayGroup, programName) => {
return new BufferGroup(arrayGroup, this.programInterfaces[programName]);
});
}
}
Expand All @@ -63,7 +53,7 @@ class Bucket {
this.recalculateStyleLayers();

for (const feature of features) {
if (this.layer.filter(feature)) {
if (this.layers[0].filter(feature)) {
this.addFeature(feature);
options.featureIndex.insert(feature, this.index);
}
Expand All @@ -72,143 +62,53 @@ class Bucket {
this.trimArrays();
}

/**
* Check if there is enough space available in the current array group for
* `vertexLength` vertices. If not, append a new array group. Should be called
* by `populateArrays` and its callees.
*
* Array groups are added to this.arrayGroups[programName].
*
* @param {string} programName the name of the program associated with the buffer that will receive the vertices
* @param {number} vertexLength The number of vertices that will be inserted to the buffer.
* @returns The current array group
*/
prepareArrayGroup(programName, numVertices) {
const groups = this.arrayGroups[programName];
let currentGroup = groups.length && groups[groups.length - 1];

if (!currentGroup || !currentGroup.hasCapacityFor(numVertices)) {
currentGroup = new ArrayGroup({
layoutVertexArrayType: this.programInterfaces[programName].layoutVertexArrayType,
elementArrayType: this.programInterfaces[programName].elementArrayType,
elementArrayType2: this.programInterfaces[programName].elementArrayType2,
paintVertexArrayTypes: this.paintVertexArrayTypes[programName]
});

currentGroup.index = groups.length;

groups.push(currentGroup);
}

return currentGroup;
}

/**
* Sets up `this.paintVertexArrayTypes` as { [programName]: { [layerName]: PaintArrayType, ... }, ... }
*
* And `this.arrayGroups` as { [programName]: [], ... }; these get populated
* with array group structure over in `prepareArrayGroup`.
*/
createArrays() {
this.arrayGroups = {};
this.paintVertexArrayTypes = {};

this.arrays = {};
for (const programName in this.programInterfaces) {
this.arrayGroups[programName] = [];
this.paintVertexArrayTypes[programName] = util.mapObject(
this.programConfigurations[programName], (programConfiguration) => {
return programConfiguration.paintVertexArrayType();
});
this.arrays[programName] = new ArrayGroup(
this.programInterfaces[programName],
this.programConfigurations[programName]);
}
}

destroy() {
for (const programName in this.bufferGroups) {
const programBufferGroups = this.bufferGroups[programName];
for (let i = 0; i < programBufferGroups.length; i++) {
programBufferGroups[i].destroy();
}
this.bufferGroups[programName].destroy();
}
}

trimArrays() {
for (const programName in this.arrayGroups) {
const arrayGroups = this.arrayGroups[programName];
for (let i = 0; i < arrayGroups.length; i++) {
arrayGroups[i].trim();
}
for (const programName in this.arrays) {
this.arrays[programName].trim();
}
}

isEmpty() {
for (const programName in this.arrayGroups) {
const arrayGroups = this.arrayGroups[programName];
for (let i = 0; i < arrayGroups.length; i++) {
if (!arrayGroups[i].isEmpty()) {
return false;
}
for (const programName in this.arrays) {
if (!this.arrays[programName].isEmpty()) {
return false;
}
}
return true;
}

getTransferables(transferables) {
for (const programName in this.arrayGroups) {
const arrayGroups = this.arrayGroups[programName];
for (let i = 0; i < arrayGroups.length; i++) {
arrayGroups[i].getTransferables(transferables);
}
for (const programName in this.arrays) {
this.arrays[programName].getTransferables(transferables);
}
}

serialize() {
return {
layerId: this.layer.id,
zoom: this.zoom,
arrays: util.mapObject(this.arrayGroups, (programArrayGroups) => {
return programArrayGroups.map((arrayGroup) => {
return arrayGroup.serialize();
});
}),
paintVertexArrayTypes: util.mapObject(this.paintVertexArrayTypes, (arrayTypes) => {
return util.mapObject(arrayTypes, (arrayType) => {
return arrayType.serialize();
});
}),

childLayerIds: this.childLayers.map((layer) => {
return layer.id;
})
layerIds: this.layers.map((l) => l.id),
arrays: util.mapObject(this.arrays, (a) => a.serialize())
};
}

recalculateStyleLayers() {
for (let i = 0; i < this.childLayers.length; i++) {
this.childLayers[i].recalculate(this.zoom, FAKE_ZOOM_HISTORY);
}
}

populatePaintArrays(interfaceName, globalProperties, featureProperties, startGroup, startIndex) {
const groups = this.arrayGroups[interfaceName];
const programConfiguration = this.programConfigurations[interfaceName];

for (const layer of this.childLayers) {
for (let g = startGroup.index; g < groups.length; g++) {
const group = groups[g];
const start = g === startGroup.index ? startIndex : 0;
const length = group.layoutVertexArray.length;

const paintArray = group.paintVertexArrays[layer.id];
paintArray.resize(length);

programConfiguration[layer.id].populatePaintArray(
layer,
paintArray,
start,
length,
globalProperties,
featureProperties);
}
for (const layer of this.layers) {
layer.recalculate(this.zoom, FAKE_ZOOM_HISTORY);
}
}
}
Expand All @@ -229,11 +129,32 @@ const subclasses = {
* @returns {Bucket}
*/
Bucket.create = function(options) {
let type = options.layer.type;
if (type === 'fill' && (!options.layer.isPaintValueFeatureConstant('fill-extrude-height') ||
!options.layer.isPaintValueZoomConstant('fill-extrude-height') ||
options.layer.getPaintValue('fill-extrude-height', {zoom: options.zoom}) !== 0)) {
const layer = options.layers[0];
let type = layer.type;
if (type === 'fill' && (!layer.isPaintValueFeatureConstant('fill-extrude-height') ||
!layer.isPaintValueZoomConstant('fill-extrude-height') ||
layer.getPaintValue('fill-extrude-height', {zoom: options.zoom}) !== 0)) {
type = 'fillextrusion';
}
return new subclasses[type](options);
};

Bucket.deserialize = function(input, style) {
// Guard against the case where the map's style has been set to null while
// this bucket has been parsing.
if (!style) return;

const output = {};
for (const serialized of input) {
const layers = serialized.layerIds
.map((id) => style.getLayer(id))
.filter(Boolean);

if (layers.length === 0) {
continue;
}

output[layers[0].id] = Bucket.create(util.extend({layers}, serialized));
}
return output;
};

0 comments on commit 59f39b7

Please sign in to comment.