-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Added Buffer2 class #1526
Added Buffer2 class #1526
Conversation
buffer_shim stuff is temporary until you move over everything to Buffer2, right? |
this._refreshArrayBufferViews(); | ||
} | ||
|
||
util.assert(this.type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lucaswoj wanna move this to the top of the function so we don't do the above work if we're just going to throw an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, if it we're actually throwing an error, performance doesn't matter much :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true, still, better to do validation before the work not after, if only for readability :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an overloaded constructor. Like Buffer1
, it can either
- create an empty buffer from a configuration object
- rehydrate a serialized instance of
Buffer
The idea with the util.assert(this.type)
line is that we allow each case to parse the constructor arguments independently and then perform a little validation on the resulting instance. It is not necessary to perform this assertion but I found that this.type
had a tendency to end up as undefined
and that resulted in hard-to-debug errors.
In a larger sense, I sorta want to rewrite this whole constructor to eliminate this branching altogether. It should be possible to combine these two cases into one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also consider removing asserts once the code has been tested and finalized. It's kinda all or nothing — either we add asserts everywhere throughout the whole code bringing a lot of cruft with it, or don't add them and rely on tests and throwing specific errors in special cases instead.
@mourner Yes, the "shim" is a temporary measure to DRY up the code that wraps a My intention is for the shim to get merged into master temporarily so we can start testing |
Are they incompatible enough to write a shim? I see that it proxies most of the methods. Could you just make Buffer2 have things that other code relies on in Buffer1, and then remove any cruft after rewriting that code? |
da3266c
to
fbe0a60
Compare
components: 3, | ||
type: Buffer2.AttributeType.UNSIGNED_SHORT | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could those move to the prototype below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the attributes could move to the prototype, but ideally they wouldn't move to the prototype. The raison-d'etre of Buffer2
is that it has a configurable buffer layout (vs Buffer1
which had a hardcoded buffer layout). Layout is intended to be passed as a constructor argument at runtime. This code represents an intermediary step towards that goal.
@lucaswoj looks great! Let's figure out the fill issues before proceeding. |
if (!condition) { | ||
throw new Error(message || 'Assertion failed'); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just use console.assert
instead of a custom function here? In addition to making it unnecessary, this has an advantage of being supported by https://github.com/twada/unassertify
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like console.assert
is not and will not be widely supported. util.assert
is just a shorthand around if (blah) throw new Error(...)
. Validating preconditions is especially helpful because WebGL is bad at reporting errors. I can remove the util.assert
statements before merging this code if you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ref #1543
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I'm convinced on assertions — let's keep, but looks like we'll use the assert
module so that it works with unassertify.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just merged #1557, so if you rebase on master you can start using assert
.
Latest commit addresses PR feedback, renames |
FPS benchmarks look 👍 so far
|
I would expect performance effects to be largely confined to the Worker. The end result is the same on the GPU, so FPS wouldn't change, but the time taken to build the Buffer in the background could change. It would be nice to have some benchmarks for that. |
@jfirebaugh How about we add a benchmark that measures the total time spent creating buffers during a slow |
A very unscientific test using the new |
d714667
to
5287956
Compare
Thanks to @mcwhittemore, we now have all buffers rewritten using Buffer2 and... drumroll... Its about 10x slower than master
I haven't even begun to think about performance optimizations, so I'm hopeful that there'll be some easy gains. |
Got the
This is roughly 2x slower than |
Just as I predicted a while back. I could take a look at optimizing this more. In the worst case, if we can't make any more perf improvements and it's still too slow, there's an option of code generation, similar to what I did with layer filters. It can bring us back to master performance while being dynamically generated from attribute config. |
* it can be a single value instead of an item. | ||
*/ | ||
Buffer.prototype.set = function(index, item) { | ||
if (!Array.isArray(item)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Performance optimization: eliminate this condition. Either:
- Always require an array, or
- Write
this.set = function() { ... }
dynamically in the constructor, and make it require an array for multiple attributes, and require no array for single attributes. For multiple attributes, it may help to iterate overoptions.attributes
rather thanitem
(like, maybe the compiler will detect thatoptions.attributes
is constant).
Writing this.set = function() { ... }
dynamically in the constructor can hopefully give you some nice speed ups, while stopping just short of needing code-gen + eval
.
this.shorts[pos2 + 1] = (Math.floor(point.y) * 2) | ty; | ||
|
||
this.bytes[pos + 4] = Math.round(extrudeScale * extrude.x); | ||
this.bytes[pos + 5] = Math.round(extrudeScale * extrude.y); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LineVertexBuffer::add
is one of the hottest functions in the codebase. It looks like dropping this call to Math.round
gives us a substantial performance boost but changes the output just enough to break test-suite. Can we afford to modify test-suite for this? Can we leverage this information in another way? cc @mourner @jfirebaugh
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @kkaefer -- the rounding here goes way back, at least as far as 5cf0c0b#diff-ede55fc20965ad828d7e5cf6eaa5c404R50.
Just wrote up a little code-gen prototype. With the removal of
... without the removal of
I've openend a separate ticket for this discussion at #1573 |
@lucaswoj if the methods are generated now, why is it slower with |
One of the things that could cause slowdowns is the need to create new objects and arrays for every push. Now we have the following syntax: this.push({
pos: [
(point.x << 1) | tx,
(point.y << 1) | ty
],
data: [
EXTRUDE_SCALE * extrude.x,
EXTRUDE_SCALE * extrude.y,
Math.round(EXTRUDE_SCALE * extrude.x),
Math.round(EXTRUDE_SCALE * extrude.y)
]
}); Can we change the generated code to turn this into e.g. just the following? this.push(
(point.x << 1) | tx,
(point.y << 1) | ty,
EXTRUDE_SCALE * extrude.x,
EXTRUDE_SCALE * extrude.y,
Math.round(EXTRUDE_SCALE * extrude.x),
Math.round(EXTRUDE_SCALE * extrude.y)); |
Yep, that seems to do it. Sketched out the idea in bea8d6d. On my machine, the bench produces: 539 ms, 128 samples for /dist/mapbox-gl.js # master
741 ms, 128 samples for /dist/mapbox-gl.js # buffer2
566 ms, 128 samples for /dist/mapbox-gl.js # flat-push
558 ms, 129 samples for /dist/mapbox-gl.js # flat-push, no Math.round For some reason removing Also note how the buffer code could be simplified further with this flat approach. |
On a side note, just noticed that |
attributes: [{ | ||
name: 'pos', | ||
components: 2, | ||
type: Buffer.AttributeType.UNSIGNED_SHORT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be SHORT
rather than UNSIGNED_SHORT
.
Also, Generally I see that we're in a great position to remove To eliminate add: function(x, y, extrudeX, extrudeY) {
this.push(
(x * 2) + ((extrudeX + 1) / 2),
(y * 2) + ((extrudeY + 1) / 2));
},
...
circleVertex.add(x, y, -1, -1);
circleVertex.add(x, y, 1, -1); Becomes: circleVertex.push(x * 2, y * 2);
circleVertex.push(x * 2 + 1, y * 2); We also may want to keep the multiply-floor packing logic in attribute configs so that things like this are not a bucket concern: this.push(
x, y,
Math.floor(ox * 64), // use 1/64 pixels for placement
Math.floor(oy * 64),
Math.floor(tx / 4), /* tex */
Math.floor(ty / 4), /* tex */ To do this, we simply add a this.push(
x, y,
ox, oy,
tx, ty,
...
attributes: [{
name: 'pos',
components: 2,
type: Buffer.AttributeType.SHORT
}, {
name: 'extrude',
components: 2,
type: Buffer.AttributeType.SHORT,
multiplier: 64, // use 1/64 pixels for placement
}, {
name: 'data1',
components: 4,
type: Buffer.AttributeType.UNSIGNED_BYTE,
multiplier: 1 / 4
}, { I'm up for dropping To eliminate custom gl.vertexAttribPointer(shader.a_pos, 2, gl.SHORT, false, stride, offset + 0);
gl.vertexAttribPointer(shader.a_offset, 2, gl.SHORT, false, stride, offset + 4);
gl.vertexAttribPointer(shader.a_data1, 4, gl.UNSIGNED_BYTE, false, stride, offset + 8);
gl.vertexAttribPointer(shader.a_data2, 2, gl.UNSIGNED_BYTE, false, stride, offset + 12); To make things more confusing, vertexAttribPointer configuration happens in two places. For some buffer types like line vertex, it's inside |
Feel free to fast-forward |
Thanks for the 👀 @mourner! I appreciate your thoughtful feedback. Below, I've enumerated your proposed next steps and added my own thoughts. add a
|
@lucaswoj good plan, let's move with this!
Actually I think we can leave this for a separate PR. It probably doesn't affect performance too much, I was looking forward to it mostly because it removes unnecessary code and keeps all buffer logic in one neat place.
Also |
I think this is ready to 🚢! Any last words?
|
|
||
body += 'var index = this.length++;\n'; | ||
body += 'var offset = index * ' + this.itemSize + ';\n'; | ||
body += 'if (offset + ' + this.itemSize + ' > this.capacity) { this._resize(this.capacity * 1.5); }\n'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NVM. Offset is what's changing here.
Going to 🚢 as soon as CI passes |
Not sure why you squashed everything into one commit, both me and John where not in favor of that... |
Oh, sorry, I see the problem now — it was hard to rebase in a way where each commit is in a good state, so squashing is good here. Fair enough. |
Sorry about the squash, @mourner. I tried to come up with a clean rebase but there were a bugs and accidental check-ins in the intermediate commits. |
recut from #1414
Up until now, GL JS has used hand-coded classes to specify buffer layouts and buffer manipulation operations. This worked well pre-data-driven-styling because there were < 10 possible buffer layouts. For example, here is the
TriangleElementBuffer
classPost-data-driven styling, however, the number of possible buffer layouts is practically infinite because any subset of paint properties might be included in the buffer.
The first step towards proper data driven styling was to create a more general and powerful framework for buffers (temporarily called
Buffer2
) which allows buffer layouts to be specified by configuration rather than code. UsingBuffer2
, We could write the sameTriangleElementBuffer
class as... but in practice we wouldn't write it that way. GL JS would choose the attributes for that buffer at runtime for the best performance.
The
Buffer2
class will know how to construct, write to, and read from GL buffers but have no semantic understanding of their contents.Instances of
Buffer2
can be (destructively) serialized and transferred across threads.cc @tmcw @mourner @kkaefer