playcanvas / playcanvas/engine
TransformFeedback: buffer allocation and usage requirements are implicit
@mvaligursky is already working on this.
Since Jul 28, 2026.
- Dominant language
- JavaScript
- Stars
- 16.8k
- Forks
- 2k
- Avg merge
- 4h 32m
- Merged PRs (30d)
- 222
Description
Two TransformFeedback setup requirements are currently implicit — neither is discoverable from the API, and both fail in ways that do not point at the cause. Found while building the graphics/transform-feedback-separate example (#9131).
1. A buffer written only by the GPU still needs initial data
A vertex buffer that transform feedback fills, and that nothing ever uploads to, must still be constructed with data — otherwise it never gets GPU storage:
// broken - GL buffer is never allocated
const instances = new VertexBuffer(device, format, count, { usage: BUFFER_GPUDYNAMIC });
// works
const instances = new VertexBuffer(device, format, count, {
usage: BUFFER_GPUDYNAMIC,
data: new Float32Array(count * 4) // never read, only forces allocation
});
Cause is in the VertexBuffer constructor — without data it allocates CPU-side storage and stops there, so setData/unlock never runs and no bufferData call is made:
const initialData = options?.data;
if (initialData) {
this.setData(initialData);
} else {
this.storage = new ArrayBuffer(this.numBytes);
}
The failure is remote from the cause. gl.getBufferParameter(ARRAY_BUFFER, BUFFER_SIZE) returns 0, bindBufferBase binds nothing, and beginTransformFeedback returns GL_INVALID_OPERATION because a binding point the program writes has no buffer. Nothing mentions the buffer that was never allocated, and there is no assert. It took a while to track down from that starting point.
Passing zeroed data purely to trigger allocation is also a real cost — count * 4 floats uploaded and thrown away for a buffer whose contents are immediately overwritten by the GPU.
Possible fixes:
- Allocate GPU storage in the constructor regardless of whether
datawas supplied - Or add an explicit way to say "allocate, do not upload"
- Or, at minimum, assert in
setTransformFeedbackBuffersthat each buffer has GPU storage, so the error names the actual problem
2. BUFFER_GPUDYNAMIC is effectively required, but silently worked around
TransformFeedback re-uploads the buffer to change its usage when it was not created with BUFFER_GPUDYNAMIC:
if (usage === BUFFER_GPUDYNAMIC && outVB.usage !== usage) {
// have to recreate input buffer with other usage
gl.bindBuffer(gl.ARRAY_BUFFER, outVB.impl.bufferId);
gl.bufferData(gl.ARRAY_BUFFER, outVB.storage, gl.DYNAMIC_COPY);
}
It works, but silently — the caller gets an extra full re-upload per buffer with no indication, and nothing tells them the usage they chose was inappropriate. A Debug.warnOnce naming the buffer would make the cost visible and steer people to BUFFER_GPUDYNAMIC up front.
Notes
Neither blocks anything — both are documented as workarounds in playcanvas/developer-site#1115. Raising them so the docs can eventually drop those caveats rather than enshrine them.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.