Rows-only element-texture uploads on three's WebGPU backends (#2944).
Element data lives in RGBA32F DataTextures whose writers register the
dirty span as per-row updateRanges (./element-storage). The classic
WebGLRenderer honours them; three r185's WebGPURenderer backends do
not — both the native WebGPU backend (WebGPUTextureUtils.updateTexture
→ one queue.writeTexture of the whole image) and the WebGL2 fallback
(WebGLTextureUtils.updateTexture → one whole-image texSubImage2D)
re-upload the full CAPACITY on every needsUpdate, so a playback commit
moved ~3x the bytes of the same commit on WebGL.
Interception point: installElementTextureRowUploads replaces the
backend instance's updateTexture(texture, options) ONCE, at renderer
creation. That is the single call three's common Textures.updateTexture
makes to move texel bytes, and it runs AFTER three has done all its own
bookkeeping for the update — createTexture on first use, the
version / generation stamps, and (after we return) onUpdate. The
wrapper therefore changes only WHICH bytes reach the GPU resource three
already allocated, never three's view of the texture's state: nothing
three tracks can go stale, and no later full re-upload is triggered.
Why the GPU contents equal a full upload: by induction over a GPU
resource's uploads. Every CPU write to an element texture registers its
span as a range, and ranges are consumed only by an upload, so the
texels OUTSIDE the pending ranges already hold their current CPU value
on the GPU whenever the resource is "in sync". Uploading the whole rows
the ranges touch, from the same image.data, then leaves every texel
equal to what a full upload would have written, and the ranges are
CONSUMED (cleared), as the classic renderer does, so the next commit
uploads only its own rows. A resource is in sync when either
this wrapper last brought exactly this resource object fully up to
date (a full upload or a ranged one), or
the texture has NEVER been uploaded by any renderer (its onUpdate
has never fired): its backing store was zero-filled at attach and
every write since is still in the pending ranges, while three has just
allocated the resource and both APIs zero-initialise new textures.
This is the ranged first upload the classic renderer already does, and
it is what keeps growth-headroom rows off a progressive load.
Three's full upload is used instead (the original method, verbatim)
whenever any of these holds:
the texture is not an element texture (every other texture);
no ranges are pending, or a full upload is pending
(markElementTextureFullDirty, the ≥75%-dirty fold, context restore);
the resource is not in sync — a reallocation (three hands back a new
GPUTexture / WebGLTexture), a device / context loss that recreates
resources, or a texture another renderer or backend already uploaded;
the texture or its GPU resource is not the exact shape this writer
knows (RGBA32F 2D, no mips, no flipY, image.data a full-size
Float32Array, descriptor size equal to the image);
a range is out of bounds;
the row write throws.
Pool re-acquire is ordinary here: the geometry keeps its texture and GPU
resource, and the new tenant's writes register their spans like any
other commit.
The rows are written through the backend's own device.queue /
gl, whose upload methods ./upload-counters wraps, and are passed as
an exact subarray so the counters tally the bytes actually written.
The classic WebGLRenderer has no backend and is left alone.
Rows-only element-texture uploads on three's WebGPU backends (#2944).
Element data lives in RGBA32F
DataTextures whose writers register the dirty span as per-rowupdateRanges(./element-storage). The classicWebGLRendererhonours them; three r185'sWebGPURendererbackends do not — both the native WebGPU backend (WebGPUTextureUtils.updateTexture→ onequeue.writeTextureof the whole image) and the WebGL2 fallback (WebGLTextureUtils.updateTexture→ one whole-imagetexSubImage2D) re-upload the full CAPACITY on everyneedsUpdate, so a playback commit moved ~3x the bytes of the same commit on WebGL.Interception point: installElementTextureRowUploads replaces the backend instance's
updateTexture(texture, options)ONCE, at renderer creation. That is the single call three's commonTextures.updateTexturemakes to move texel bytes, and it runs AFTER three has done all its own bookkeeping for the update —createTextureon first use, theversion/generationstamps, and (after we return)onUpdate. The wrapper therefore changes only WHICH bytes reach the GPU resource three already allocated, never three's view of the texture's state: nothing three tracks can go stale, and no later full re-upload is triggered.Why the GPU contents equal a full upload: by induction over a GPU resource's uploads. Every CPU write to an element texture registers its span as a range, and ranges are consumed only by an upload, so the texels OUTSIDE the pending ranges already hold their current CPU value on the GPU whenever the resource is "in sync". Uploading the whole rows the ranges touch, from the same
image.data, then leaves every texel equal to what a full upload would have written, and the ranges are CONSUMED (cleared), as the classic renderer does, so the next commit uploads only its own rows. A resource is in sync when eitheronUpdatehas never fired): its backing store was zero-filled at attach and every write since is still in the pending ranges, while three has just allocated the resource and both APIs zero-initialise new textures. This is the ranged first upload the classic renderer already does, and it is what keeps growth-headroom rows off a progressive load.Three's full upload is used instead (the original method, verbatim) whenever any of these holds:
markElementTextureFullDirty, the ≥75%-dirty fold, context restore);GPUTexture/WebGLTexture), a device / context loss that recreates resources, or a texture another renderer or backend already uploaded;image.dataa full-size Float32Array, descriptor size equal to the image);Pool re-acquire is ordinary here: the geometry keeps its texture and GPU resource, and the new tenant's writes register their spans like any other commit.
The rows are written through the backend's own
device.queue/gl, whose upload methods./upload-counterswraps, and are passed as an exactsubarrayso the counters tally the bytes actually written.The classic
WebGLRendererhas no backend and is left alone.