perf: use immutable storage for WebGL data textures - #1835
Conversation
LaurenzV
left a comment
There was a problem hiding this comment.
LGTM! I'm sorry for being so nitpicky about the comments. 😦 If you find it annoying, feel free to disregard my proposed changes.
The reason why I personally think it's worth paying attention to is simply because having those lengthy comments makes future code reviews harder, and unfortunately AI has a tendency to add comments for everything. Because in case anything ever changes, not only do we need to re-review all of the code, but we also need to reread and verify all the comments, so it adds additional churn.
So for example instead of referring to the storage strategy from all the different callers, I think it's better to just specify it once where it really happens. And in case users want to read more about the specific advantages of texStorage2D they can simply open the link or do their own research, I don't think we need to document all of this in the code itself, we only need to document the minimum so the rationale for why we wrote the code the way it is now can be understood later on in case anyone reads the code.
But if you don't mind this let me know and I won't point it out in the future anymore. 😄
2d4dd21 to
78f9207
Compare
|
Simplified the doc comments as suggested. |
Problem
The WebGL renderer calls
texImage2Devery frame for alpha, encoded-paint, gradient, and filter-data textures. This uploads the texture’s full peak allocation even when the current frame only uses a few rows.After one large frame grows a texture, subsequent small frames continue paying the peak-sized upload cost.
Root Cause
texImage2Ddoes more than copy pixels: it specifies the texture storage for a mip level, including its dimensions and internal format.respecification: the call tells WebGL that the existing storage may be replaced with a differently sized or formatted allocation. Even when the arguments remain unchanged, the API permits a storage change, so the browser and driver may need to:
By contrast,
texSubImage2Dcannot change the allocation. It only writes pixels into storage whose size and format are already fixed, giving the implementation stronger guarantees.The renderer’s CPU buffers and texture heights only grow, so the old
texImage2Dcalls also upload the largest allocation observed rather than the rows required by the current frame.Solution
Allocate these data textures with immutable
texStorage2Dstorage and update their contents usingtexSubImage2D.Expected Performance Impact
The improvement should primarily appear in CPU time inside
WebGlRenderer::render, specifically during per-frame texture uploads:The largest improvement should occur when a heavy frame is followed by smaller frames. Previously, every smaller frame continued re-specifying and uploading the peak-sized texture; now it uploads only its active rows.
Frames that grow a texture still require a new allocation, but growth is infrequent because capacity only increases. Frames that genuinely use the full peak allocation will transfer approximately the same amount of data as before.
References
MDN: WebGL best practices — Use
texStorageto create textures explains that independently specifiedtexImagemip levels prevent drivers from preparing texture memory until draw time. It recommendstexStorage+texSubImagefor WebGL 2 and notes that some drivers may otherwise allocate an unnecessary full mip chain, adding approximately 30% memory.MDN:
texStorage2Ddocuments allocating texture levels with fixed dimensions and format.Khronos: Texture Storage explains the fundamental difference between mutable and immutable allocation. Mutable storage is defined one mip level at a time with
glTexImage*, and those levels can later be replaced with different dimensions or formats. This leaves the application responsible for maintaining texture completeness. Immutable storage allocates all requested levels together with fixed dimensions and formats. Importantly, “immutable” applies only to the allocation — not its pixel contents. The contents can still be replaced repeatedly withglTexSubImage*. For Vello, this means the backing allocation remains stable between frames while frame-specific data can still be uploaded normally.Khronos: Pixel Transfer explicitly separates allocation from updating:
glTexImage*allocates mutable storage for a mip level and optionally writes pixel data.glTexSubImage*only writes pixel data into existing storage.This distinction is directly relevant to Vello. The old code used the allocation-capable
texImage2Doperation for every frame update, even when the texture dimensions and format had not changed. The new code usestexStorage2Donly when creating or growing a texture, then uses the update-onlytexSubImage2Dpath during normal frames. That communicates to the driver that no allocation or format change is possible during routine uploads.These Khronos pages describe OpenGL, but the same storage-versus-content model underlies the equivalent WebGL 2 APIs. They establish the semantic reason for using
texStorage2D; they do not by themselves guarantee a particular performance improvement.Created with assistance from GPT-5.6 Sol.