Skip to content

Commit

Permalink
support multiple ranges in attribute partial update (#3119)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed May 23, 2019
1 parent c7f9c02 commit 0389f01
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 45 deletions.
5 changes: 5 additions & 0 deletions docs/api-reference/attribute-manager.md
Expand Up @@ -102,12 +102,17 @@ Mark an attribute as need update.
Parameters:

* `name` (String) - Either the name of the attribute, or the name of an accessor. If an name of accessor is provided, all attributes with that accessor are invalidated.
* `dataRange` (Object, optional) - A partial range of the attribute to invalidate, in the shape of `{startRow, endRow}`. Start (included) and end (excluded) are indices into the data array. If not provided, recalculate the attribute for all data.


##### `invalidateAll`

Mark all attributes as need update.

Parameters:

* `dataRange` (Object, optional) - A partial range of the attributes to invalidate, in the shape of `{startRow, endRow}`. Start (included) and end (excluded) are indices into the data array. If not provided, recalculate the attributes for all data.


##### `update`

Expand Down
12 changes: 6 additions & 6 deletions modules/core/src/lib/attribute-manager.js
Expand Up @@ -191,18 +191,18 @@ export default class AttributeManager {
}

// Marks an attribute for update
invalidate(triggerName) {
const invalidatedAttributes = this._invalidateTrigger(triggerName);
invalidate(triggerName, dataRange) {
const invalidatedAttributes = this._invalidateTrigger(triggerName, dataRange);
// For performance tuning
logFunctions.onLog({
level: LOG_DETAIL_PRIORITY,
message: `invalidated attributes ${invalidatedAttributes} (${triggerName}) for ${this.id}`
});
}

invalidateAll() {
invalidateAll(dataRange) {
for (const attributeName in this.attributes) {
this.attributes[attributeName].setNeedsUpdate();
this.attributes[attributeName].setNeedsUpdate(attributeName, dataRange);
}
// For performance tuning
logFunctions.onLog({
Expand Down Expand Up @@ -369,15 +369,15 @@ export default class AttributeManager {
this.updateTriggers = triggers;
}

_invalidateTrigger(triggerName) {
_invalidateTrigger(triggerName, dataRange) {
const {attributes, updateTriggers} = this;
const invalidatedAttributes = updateTriggers[triggerName];

if (invalidatedAttributes) {
invalidatedAttributes.forEach(name => {
const attribute = attributes[name];
if (attribute) {
attribute.setNeedsUpdate();
attribute.setNeedsUpdate(attribute.id, dataRange);
}
});
} else {
Expand Down
73 changes: 48 additions & 25 deletions modules/core/src/lib/attribute.js
Expand Up @@ -4,13 +4,15 @@ import {Buffer} from '@luma.gl/core';
import assert from '../utils/assert';
import {createIterable} from '../utils/iterable-utils';
import {fillArray} from '../utils/flatten';
import * as range from '../utils/range';
import log from '../utils/log';
import BaseAttribute from './base-attribute';

const DEFAULT_STATE = {
isExternalBuffer: false,
needsUpdate: true,
needsRedraw: false,
updateRanges: range.FULL,
allocedInstances: -1
};

Expand Down Expand Up @@ -141,11 +143,19 @@ export default class Attribute extends BaseAttribute {
return null;
}

// Checks that typed arrays for attributes are big enough
// sets alloc flag if not
// @return {Boolean} whether any updates are needed
setNeedsUpdate(reason = this.id) {
setNeedsUpdate(reason = this.id, dataRange) {
this.userData.needsUpdate = this.userData.needsUpdate || reason;
if (dataRange) {
const {startRow = 0, endRow = Infinity} = dataRange;
this.userData.updateRanges = range.add(this.userData.updateRanges, [startRow, endRow]);
} else {
this.userData.updateRanges = range.FULL;
}
}

clearNeedsUpdate() {
this.userData.needsUpdate = false;
this.userData.updateRanges = range.EMPTY;
}

setNeedsRedraw(reason = this.id) {
Expand All @@ -168,6 +178,7 @@ export default class Attribute extends BaseAttribute {
// Allocate at least one element to ensure a valid buffer
const allocCount = Math.max(numInstances, 1);
const ArrayType = glArrayFromType(this.type || GL.FLOAT);
const oldValue = this.value;

this.constant = false;
this.value = new ArrayType(this.size * allocCount);
Expand All @@ -176,47 +187,59 @@ export default class Attribute extends BaseAttribute {
this.buffer.reallocate(this.value.byteLength);
}

state.needsUpdate = true;
if (state.updateRanges !== range.FULL) {
this.value.set(oldValue);
// Upload the full existing attribute value to the GPU, so that updateBuffer
// can choose to only update a partial range.
// TODO - copy old buffer to new buffer on the GPU
this.buffer.subData(oldValue);
}

this.setNeedsUpdate(true, {startRow: instanceCount});
state.allocedInstances = allocCount;
return true;
}

return false;
}

updateBuffer({numInstances, bufferLayout, data, startRow, endRow, props, context}) {
updateBuffer({numInstances, bufferLayout, data, props, context}) {
if (!this.needsUpdate()) {
return false;
}

const state = this.userData;

const {update} = state;
const {update, updateRanges} = state;

let updated = true;
if (update) {
// Custom updater - typically for non-instanced layers
update.call(context, this, {data, startRow, endRow, props, numInstances, bufferLayout});
for (const [startRow, endRow] of updateRanges) {
update.call(context, this, {data, startRow, endRow, props, numInstances, bufferLayout});
}
if (this.constant || !this.buffer || this.buffer.byteLength < this.value.byteLength) {
// Full update
// call base clas `update` method to upload value to GPU
this.update({
value: this.value,
constant: this.constant
});
} else {
const startOffset = Number.isFinite(startRow)
? this._getVertexOffset(startRow, this.bufferLayout)
: 0;
const endOffset =
Number.isFinite(endRow) || !Number.isFinite(numInstances)
? this._getVertexOffset(endRow, this.bufferLayout)
: numInstances * this.size;

// Only update the changed part of the attribute
this.buffer.subData({
data: this.value.subarray(startOffset, endOffset),
offset: startOffset * this.value.BYTES_PER_ELEMENT
});
for (const [startRow, endRow] of updateRanges) {
const startOffset = Number.isFinite(startRow)
? this._getVertexOffset(startRow, this.bufferLayout)
: 0;
const endOffset =
Number.isFinite(endRow) || !Number.isFinite(numInstances)
? this._getVertexOffset(endRow, this.bufferLayout)
: numInstances * this.size;

// Only update the changed part of the attribute
this.buffer.subData({
data: this.value.subarray(startOffset, endOffset),
offset: startOffset * this.value.BYTES_PER_ELEMENT
});
}
}
this._checkAttributeArray();
} else {
Expand All @@ -225,7 +248,7 @@ export default class Attribute extends BaseAttribute {

this._updateShaderAttributes();

state.needsUpdate = false;
this.clearNeedsUpdate();
state.needsRedraw = true;

return updated;
Expand Down Expand Up @@ -255,7 +278,7 @@ export default class Attribute extends BaseAttribute {
this.update({constant: true, value});
}
state.needsRedraw = state.needsUpdate || hasChanged;
state.needsUpdate = false;
this.clearNeedsUpdate();
state.isExternalBuffer = true;
this._updateShaderAttributes();
return true;
Expand All @@ -268,7 +291,7 @@ export default class Attribute extends BaseAttribute {

if (buffer) {
state.isExternalBuffer = true;
state.needsUpdate = false;
this.clearNeedsUpdate();

if (buffer instanceof Buffer) {
if (this.externalBuffer !== buffer) {
Expand Down
46 changes: 46 additions & 0 deletions modules/core/src/utils/range.js
@@ -0,0 +1,46 @@
/*
* range (Array)
* + start (Number) - the start index (incl.)
* + end (Number) - the end index (excl.)
* rangeList (Array) - array of sorted, combined ranges
*/
export const EMPTY = [];
export const FULL = [[0, Infinity]];

// Insert a range into a range collection
export function add(rangeList, range) {
// Noop if range collection already covers all
if (rangeList === FULL) {
return rangeList;
}

// Validate the input range
if (range[0] < 0) {
range[0] = 0;
}
if (range[0] >= range[1]) {
return rangeList;
}

// TODO - split off to tree-shakable Range class
const newRangeList = [];
const len = rangeList.length;
let insertPosition = 0;

for (let i = 0; i < len; i++) {
const range0 = rangeList[i];

if (range0[1] < range[0]) {
// the current range is to the left of the new range
newRangeList.push(range0);
insertPosition = i + 1;
} else if (range0[0] > range[1]) {
// the current range is to the right of the new range
newRangeList.push(range0);
} else {
range = [Math.min(range0[0], range[0]), Math.max(range0[1], range[1])];
}
}
newRangeList.splice(insertPosition, 0, range);
return newRangeList;
}

0 comments on commit 0389f01

Please sign in to comment.