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
Allow user to copy full buffer with gl.bufferData
#9631
Conversation
Shouldn't we just change the code so it looks like this instead? if ( data.dynamic === false || data.updateRange.count === - 1 ) {
// Not using update ranges
var usage = data.dynamic ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW;
gl.bufferData( bufferType, data.array, usage );
} else ... |
Yup, although there may be a performance impact on some GPUs when using I'm guessing that's why this uses |
So I guess what you really want is to dispose the current buffer? |
Hmm nope, bufferData is used to allocate the array, which is needed if you have for e.g. a larger buffer than before. You do not need to dispose/delete any buffers to do this. Example:
|
Hmm, but |
The bufferData call overwrites previous data (with new data, usage and size) so there is no leak. It replaces the old data. |
@kenrussell any pointers on whether this is good practice? or whether we should be calling |
I think it's the best practice to not call gl.deleteBuffer and allocate a new one, but to optionally use gl.bufferData to reallocate the entire buffer object, as @mattdesl is suggesting. Deleting the old buffer and creating a new one does cause unnecessary churn. |
@kenrussell thanks! |
@kenrussell One more question actually... Is is slower to use |
I think it's safest to continue to use bufferSubData to update same-sized buffers. There are arguments on both sides about it and I think it may actually be GPU-dependent whether it's faster or slower than bufferData. Chrome has some code which chooses the faster implementation in some cases. But Three.js seems to work just fine on a large range of devices at this point so I'd say if it isn't broken, don't fix it. |
@kenrussell thanks! |
Struggling to find a nice API for this... |
There is a problem earlier in the process. A BufferAttribute's size is fixed at creation, when a Geometry vertices etc are expanded after GPU upload, The updates are processed by bufferAttribute.copy{x}( ) when updating the attached BufferGeometry. This currently fails silently ( at least in Chrome). I hit this earlier this week, and resorted to copying into a new geometry,. Maybe detect mismatch between Geometry attribute size and buffer attribute size at time of update. Then you could create a new TypedArray object of the correct size and at the same time trigger the use of gl.BufferData call (not something I have any insight into). This would mainly be contained in BufferAttribute.copy() methods with small mods to the renderer for the gl call variation. Thoughts? Maybe totally wrong here, still learning. This would be totally transparent and not even require a user facing API. |
@aardgoose Where is the buffer size fixed? If we wanted to make this fully hidden to the user (i.e. they have no control over I think the simplest approach is just to place this on the end-user. For the API — instead of a boolean, you could use a "strategy" flag: var attr = new BufferAttribute(..);
attr.bufferStrategy = THREE.BufferDataStrategy;
// attr.bufferStrategy = THREE.BufferSubDataStrategy; Or something like that. |
That is the issue - the BufferAttribute internal TypedArray is fixed at creation time ie: new" BufferAttribute();". Typed arrays are fixed size, there isn't a method of expanding them. The copy() methods used when the buffer attributes are changed simply silently ignore writes pass the end of the array so: var a = new Uint8Array( [ 0, 1, 2, 3 ] ); // create length 4 array
The TypedArray.set() method is the only access method that seems to care and raises an exception. So to have variable sized BufferAttributes, the .array typedArray needs to be change on demand, the expanded geometry currently never gets near the gl.* calls, it fails before then (silently). |
Sure, but the user should be able to do this: attrib.array = new Float32Array(...);
attrib.needsUpdate = true; 😄 |
True, there is nothing to stop them doing that, but there is nothing that says that it should work, it's fiddling with what is really a private property of the object, or would be in another language. If it breaks something, bad luck. Look at Object3D etc when certain properties like position have been made un-replaceable to stop foot/gun incidents. If you checked incoming length on the copy methods you could get automatic resizing after altering a Geometry() with no user changes, and if you really want to play directly with the BufferAttribute like that, surely it would be better to use the copyArray() method and adjust that to check for length changes, and you could get needsUpdate and any other flag setting for free. Simple for the user and insulates them from any changes in the future. Resizing, however done is going to be fairly costly, so having a proper API won't add much overhead. Anyway, not my library or decision, I am just very happy that it exists and works well. |
I didn't think I'm pretty OK with any change that lets AttributeBuffer grow/shrink dynamically. The documentation should be easy enough to fix. I can change my PR if we decide on the best way to go. |
@mattdesl Can you allocate a sufficiently-large, fixed-sized buffer and use |
What I meant that I think it would be private in other languages, rather than actually is (I probably should have said that more explicitly) I imagine most people use the standard method, at least most of Three.js objects do. If we have a method to allow new arrays of different sizes to be setup as suggested by mrdoob in the other PR, that allows the count field to be maintained, variable array size and the discard method can my later PR can co-exist happily, which is my personal concern. Plus it hides renderer nicely. |
@WestLangley yup, but it's a pain and eventually will break when your array is not "sufficiently large enough." It's not a good solution IMHO when |
How about adding a |
I pushed a change to update the API with
Hmm, the only issue is that the method introduces a side-effect that users might not be expecting. For example, they might use We could check in We could call the boolean |
@mrdoob @aardgoose What do you guys think? |
I don't have any issues, I think you worries about side effects regarding strategies shouldn't be an issue, It is a new API so there are no prior expectations. Adding some documentation for setArray() that makes this behaviour explicit would help. |
Hmm... @kenrussell suggested that maybe a better API would be adding a |
I am talking about side-effects in the functional sense. Generally, it's not good for methods to introduce implicit side-effects, and it's better to force the user to explicitly mutate state. Let's say setSize: function ( arrayLength ) {
if ( typeof arrayLength !== 'number' ) {
throw new TypeError('Must specify a number as arrayLength to setSize()');
}
var oldCount = this.count;
this.count = arrayLength / this.itemSize;
if ( !this.needsFullBuffer && this.count !== oldCount ) {
this.needsFullBuffer = true;
}
} Now take the following end-user code (which is not uncommon, since some users set the data some time after the constructor). const attr = new THREE.BufferAttribute();
...
attr.itemSize = 2;
attr.array = new Float32Array(myData);
attr.setSize(attr.array.length); The unexpected thing here is that now this attribute will always copy with Ideally, a graphics engine might only update the full And, more generally, attr.array = newArray;
attr.setSize(attr.array.length); Instead of the following, which has the same effect: attr.setArray(newArray); Anyways... my opinion is still to force the const attr = new THREE.BufferAttribute(data, 2);
// allow this attribute to grow/shrink
attr.needsFullBuffer = true;
...
// to grow/shrink the data
attr.setArray(newArray); |
More thinking about this... Aren't we always calling |
I updated the PR, now attr.setArray(newData);
attr.needsUpdate = true; Because needsUpdate always increments the version field, and we are only testing against |
Ugh. Yeah... Hmm, I'm starting to think that the right thing to do here is to go the dumb (thus predicable) approach and just do this: function updateBuffer( attributeProperties, data, bufferType ) {
gl.bindBuffer( bufferType, attributeProperties.__webglBuffer );
if ( data.dynamic === false ) {
gl.bufferData( bufferType, data.array, gl.STATIC_DRAW );
} else if ( data.updateRange.count === - 1 ) {
// Not using update ranges
gl.bufferSubData( bufferType, 0, data.array );
} else if ( data.updateRange.count === 0 ) {
console.error( 'THREE.WebGLObjects.updateBuffer: dynamic THREE.BufferAttribute marked as needsUpdate but updateRange.count is 0, ensure you are using set methods or updating manually.' );
} else {
gl.bufferSubData( bufferType, data.updateRange.offset * data.array.BYTES_PER_ELEMENT,
data.array.subarray( data.updateRange.offset, data.updateRange.offset + data.updateRange.count ) );
data.updateRange.count = 0; // reset range
}
attributeProperties.version = data.version;
} I realised that the use cases where this could affect performance are when users update the We also gain control on being able to use |
That sounds good to me! I'll update the PR tomorrow. 😊 |
Ok @mrdoob I've updated this PR. 😄 Let me know! Thanks. |
Thaaaanks! |
Currently all GL buffers are updated with
gl.bufferSubData
— which makes sense most of the time for performance.However, this means you need to use fixed-size buffers, and can't re-use a GL buffer with a larger or smaller array than before. This is a problem for geometry that needs to dynamically grow/shrink.
One solution is to dispose and re-create your THREE.Geometry instances every time they need to shrink/grow, but this can be a bit tedious for the end-user since they have to restructure their application logic to handle changing geometries.
Another solution is to have a flag (disabled by default) on BufferAttribute that tells the engine to update the entire contents of the array (using
gl.bufferData
) rather than only a sub-region. This way the same GL buffers can be re-used without deleting/re-creating them (causing needless noise in GL inspectors), and with the right abstractions the user will never need to change their application logic.In practice this shows up with a library like three-bmfont-text since users may choose to add/remove glyphs (quads) at runtime. This PR will allow developers like myself more control over the GL buffers, allowing cleaner and more optimized abstractions without affecting the end-user's code.
Thanks! 😄