Byte rules: a typed-array source counts the bytes the call actually reads
(honouring the WebGL2 / WebGPU element-unit srcOffset / length args); a
WebGL texture upload from a typed array with a known format / type counts
the REGION it writes (width * height * depth * bytesPerPixel, capped by
the view) — a compressed upload counts the block bytes its view hands over,
which are exactly the bytes uploaded — three's classic ranged path hands every per-row
texSubImage2D the WHOLE image.data and selects the row with
UNPACK_SKIP_ROWS, so the view length says nothing about the upload
(#2944); a writeTexture counts its region (bytesPerRow * rows, capped
by the data past offset) when the layout gives a bytesPerRow; a
numeric bufferData size counts that allocation; an image-like source
(image, canvas, ImageBitmap, video frame) counts width * height * depth * 4
when its size is determinable and 0 otherwise; a PBO offset or null counts
0. The call is counted in every case.
Installation is idempotent (the context / queue is marked with a symbol).
The wrappers are own properties of the context / queue object, which three
looks up on every call, and a WebGL context restore keeps the SAME context
object — the viewer rebuilds resources against it rather than creating a
new one — so the wrappers survive a restore without being reapplied.
Instrumentation only: every wrapper forwards its exact arguments (WebGL
overload resolution depends on argument COUNT) and returns the original's
result. The byte accounting cannot throw into the caller.
GPU upload counters — wrap the backend's upload entry points ONCE so every CPU→GPU byte the renderer pushes is tallied in perfCounters.
Counters (all cumulative):
gpu.uploadCalls— every wrapped upload call, whatever it carried.gpu.uploadBytes— bytes those calls uploaded (buffer + texture).gpu.uploadBytes.buffer—bufferData/bufferSubData/queue.writeBuffer.gpu.uploadBytes.texture—texImage2D/3D/texSubImage2D/3D/compressedTex(Sub)Image2D/3D/queue.writeTexture/queue.copyExternalImageToTexture.Byte rules: a typed-array source counts the bytes the call actually reads (honouring the WebGL2 / WebGPU element-unit
srcOffset/lengthargs); a WebGL texture upload from a typed array with a known format / type counts the REGION it writes (width * height * depth * bytesPerPixel, capped by the view) — a compressed upload counts the block bytes its view hands over, which are exactly the bytes uploaded — three's classic ranged path hands every per-rowtexSubImage2Dthe WHOLEimage.dataand selects the row withUNPACK_SKIP_ROWS, so the view length says nothing about the upload (#2944); awriteTexturecounts its region (bytesPerRow * rows, capped by the data pastoffset) when the layout gives abytesPerRow; a numericbufferDatasize counts that allocation; an image-like source (image, canvas, ImageBitmap, video frame) countswidth * height * depth * 4when its size is determinable and 0 otherwise; a PBO offset ornullcounts 0. The call is counted in every case.Installation is idempotent (the context / queue is marked with a symbol). The wrappers are own properties of the context / queue object, which three looks up on every call, and a WebGL context restore keeps the SAME context object — the viewer rebuilds resources against it rather than creating a new one — so the wrappers survive a restore without being reapplied.
Instrumentation only: every wrapper forwards its exact arguments (WebGL overload resolution depends on argument COUNT) and returns the original's result. The byte accounting cannot throw into the caller.