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
WEBGL_get_buffer_sub_data_async extension #2079
Conversation
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 excellent work @kainino0x . The tests are superb and you clearly thought through all of the edge cases when writing them.
Working group members, please review this; in particular, @jdashg @Kangz @grorg @RafaelCintron @zhenyao @junov . All of you reviewed the earlier pull request #1937 which has been closed in favor of this one.
Kai has implemented this in Chrome and it's showing excellent results. It eliminates the synchronous cost of getting data back from the GPU, and in Chrome, the time between when getBufferSubDataAsync is called and when the promise is resolved is less than when making the synchronous getBufferSubData call! This benefit will be realized for any multi-process or multi-threaded WebGL implementation. Kai will send more details and a test case to the mailing list tomorrow.
A couple of minor comments about the tests. They look basically perfect overall. The chaining of promises together in the main test is an excellent design and is comprehensible even to newcomers.
for (var i = 0; i < TEST_COUNT; i++) { | ||
(function() { | ||
var counter = i; | ||
var color = [counter & 0xff, (counter << 8) & 0xff, (counter << 16) & 0xff, 255]; |
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.
Did you mean to right shift instead of left shift these? They'll always shift in zero bits as written.
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.
Still looking for an update in this area.
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.
Whoops, missed this. Fixed.
debug(""); | ||
finishTest(); | ||
}).catch(function(e) { | ||
testFailed(e.toString()); |
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.
Should this also call finishTest so the test harness (webgl-conformance-test.html) doesn't report this as a timeout if an exception is thrown?
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, fixed
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.
Looking great. One unresolved comment from earlier.
for (var i = 0; i < TEST_COUNT; i++) { | ||
(function() { | ||
var counter = i; | ||
var color = [counter & 0xff, (counter << 8) & 0xff, (counter << 16) & 0xff, 255]; |
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.
Still looking for an update in this area.
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.
LGTM overall, a couple comments.
buf + " but expected " + color); | ||
} | ||
if (counter <= last_counter) { | ||
testFailed("getBufferSubDataAsync calls #" + last_counter + " and #" + counter + " out of order"); |
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.
The getBufferSubDataAsync promises being called in order doesn't appear in the spec (or I didn't find it) and seems to be an implementation detail. How about having a vector of bools and in Promise.all(promises).then() check that it is all true?
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.
That is a good point. The command "executes serially with respect to others in the command stream" but the promises may not need to return that way. However, I'm not totally certain whether it should be changed in the spec or the test. @kenrussell, what do you think?
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'm not sure how the guarantees about what data is seen in the ArrayBufferView would be maintained if the promises resolved out of order. The same ArrayBufferView might be used as the target to getBufferSubData multiple times, and the promises' "then" callbacks would assume the data would be observed sequentially.
Should we say something in the spec about sequential calls A and B to getBufferSubDataAsync, and that the promise for call A must be resolved before the promise to call B, assuming both are not rejected early?
It'd be fine by me to change the test. Is there a simple way to express success? I would assume that Promise.all's resolve callback would only be called upon success of all of the individual promises and their resolve callbacks -- so throwing an exception out of one of them would cause Promise.all to fail. Could you investigate?
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.
Since the ArrayBufferView isn't written until promise resolution, I don't see a problem there. We don't make any guarantees about the contents of the ArrayBufferView after the promise callback is executed; only during its execution.
Yes, failing one would cause Promise.all to fail. But there can be testFailed
messages inside the individual callbacks as well so I think that would be okay.
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.
Like Kai, I don't think we need to add ordering to the spec, the fact that all the promises are resolved on the borwsing context event loop gives the guarantee that inside the promise.then, the ArrayBufferView content is correct.
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.
OK. Given your analyses I think it's fine to leave the spec as is.
}); | ||
|
||
allPromises = allPromises.then(function() { | ||
var code = "promise = gl.getBufferSubDataAsync(gl.ARRAY_BUFFER, 0, new ArrayBuffer(4))"; |
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.
Why should these be rejected? Is it because no buffer is bound to gl.ARRAY_BUFFER (I think some might be)?
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.
Those are type checks, which I didn't make clear at all. I added debug prints to explain.
PASS correctly rejected with TypeError: Failed to execute 'getBufferSubDataAsync' on 'WebGL2RenderingContext': parameter 3 is not of type 'ArrayBufferView'.: promise = gl.getBufferSubDataAsync(gl.ARRAY_BUFFER, 0, new ArrayBuffer(4))
PASS correctly rejected with TypeError: Failed to execute 'getBufferSubDataAsync' on 'WebGL2RenderingContext': parameter 3 is not of type 'ArrayBufferView'.: promise = gl.getBufferSubDataAsync(gl.ARRAY_BUFFER, 0, null)
wtu.shouldGenerateGLError(gl, gl.INVALID_VALUE, code); | ||
return expectRejected(promise, code); | ||
}); | ||
|
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.
A couple more tests that might be useful:
- Check that getBufferSubDataAsync on an uninitialized (gl.bufferData with null) buffer returns zeros.
- Check that getBufferSubDataAsync immediately followed by a buffer resize to be too small doesn't crash.
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.
Thanks, I will add those.
<code>dstData</code>. If any of the above errors occur, the Promise is already rejected upon | ||
return. If the context is lost before the Promise is fulfilled, or if <code>dstData</code> | ||
is neutered at the time the Promise is fulfilled, then the Promise will be rejected later. | ||
In this case, no WebGL error is generated. If the promise rejects, no data is written to |
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.
@kenrussell Just want to make sure that this is the semantic we want. Right now no WebGL error will ever be generated upon resolution of the promise.
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, that sounds like the best semantic to me.
I merged master into this branch, updated the spec language to match getBufferSubData, etc. I think I've addressed all of the outstanding comments and issues. |
Still LGTM |
Added a test for the transform feedback error case (finally). Also added in getBufferSubData tests in #2097 |
… active" This reverts commit a695837.
Removed, will add it back to this PR once that one has landed and this PR's fate has stabilized. EDIT: it's added back now |
Excellent work. For a little more background for other reviewers: the new form of the spec text came from a suggestion from Elliott Sprehn from the Blink team, pointing us at other web specs that return promises. LGTM. |
I've finally moved this entire spec into an extension. The extension is implemented and tested on my local Chromium build. Aside from the new specification text, everything is as it was before. Ready for review. |
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.
LGTM with a question
Feel free to merge yourself
var promises = []; | ||
debug("kicking off " + TEST_COUNT + " getBufferSubDataAsync calls"); | ||
for (var i = 0; i < TEST_COUNT; i++) { | ||
(function() { |
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.
Curious: why make the loop body a function? why regular code will not do?
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 a javascript programming pattern that guards against a weirdness of closures, which is that they always capture variables, not values. So if you capture i
, then all of the closures (promise callbacks) will see i == TEST_COUNT
instead of the loop value.
After writing this, though, I learned about let
, which actually will solve this. I'm going to go ahead and clean this up using let
.
}); | ||
|
||
for (var i = 0; i < types.length; i++) { | ||
(function() { |
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.
Same question here.
debug("kicking off " + TEST_COUNT + " getBufferSubDataAsync calls"); | ||
for (var i = 0; i < TEST_COUNT; i++) { | ||
insertTimeoutAssertion(); | ||
(function() { |
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.
Same question here?
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 actually forgot about these tests and haven't updated them. I need to do this before I merge.
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.
What about leaving these manual tests out of this pull request?
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.
Sure, good call. Done.
lgtm |
Very nice @kainino0x and thanks @zhenyao for carrying forward the code review. LGTM after the fact. |
Add an extension for asynchronous buffer readback. Name subject to bikeshedding. Fixes KhronosGroup#1938.
I took @kenrussell's spec text from #1937 but significantly rewrote it text to exactly match the new text for getBufferSubData where appropriate. I also changed the IDL to match getBufferSubData.
This entry point solves the problem of synchronous, blocking GPU-to-CPU memory readback on the main thread.
Some performance results in real-world usage (screen picking):
https://github.com/kainino0x/getBufferSubDataAsync-Demo
verifyGetBufferSubData
)sdk/tests/extra/
.This PR fixes #1938.