[Perf] (MG_State): remember every dirty rect, not just their union

A Minecraft frame updates ~95 scattered 16x16 sprites in a 1024x512
atlas; MipmapStorage's single union dirty box turned ~95KB of changed
texels into a ~2MB upload on every backend. The storage now keeps a
bounded (96-slot) list of pairwise-disjoint dirty rects BEHIND the
untouched union box: rects cascade-merge on touch or overlap, overflow
folds the pair with minimum enlargement and re-cascades, whole-level
dirties and respecifies just clear the list (empty list = "union box
tells all"). GetDirtyRects hands the list out only when it has 2+
rects, fits the caller's capacity, and its summed area is under 75% of
the union box - fewer driver calls beat equal bytes - so consumers can
never stage more than the union box did.

The list is maintained inside the same four mutation funnels every
texel writer already goes through (MarkDirty, MarkDirtyRegion,
AllocateLevel, TruncateToLevelCount - callers enumerated at the
declaration), so list and union box cannot disagree. Backends OPT IN:
the union-box API and its update order are byte-identical, and an
unmodified backend keeps rendering exactly as before.

96 slots is measured, not guessed: on the bench's 95-sprite lattice a
16-slot list collapses to >93% of the union box, 96 slots reach 4.8%
(~2MB -> ~95KB staged per frame). Verified by a 2859-check fuzz run
against a reference dirty bitmap (union exactness, full coverage,
disjointness, bounds, profitability). Unit tests 421/421.
This commit is contained in:
BZLZHH
2026-08-06 20:17:19 -04:00
parent 990e518e33
commit 7db5b35a3e
7 changed files with 193 additions and 0 deletions
@@ -186,6 +186,20 @@ namespace MobileGL::MG_State::GLState {
const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel);
return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
}
// Scatter detail behind GetStorageDirtyRegion: up to maxRects disjoint rects
// that together cover every dirty texel, so ~100 sprite writes in a big atlas
// need not be uploaded as one atlas-sized box. Returns how many rects were
// written to outRects; 0 means "no list, upload the union box" and is always a
// safe answer - this base fallback keeps whole-level semantics for storage
// classes that do not track rects, and backends OPT IN by calling this.
virtual SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
MipmapDirtyRegion* outRects, SizeT maxRects) const {
(void)uploadTarget;
(void)mipmapLevel;
(void)outRects;
(void)maxRects;
return 0;
}
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
@@ -257,6 +271,8 @@ namespace MobileGL::MG_State::GLState {
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
IntVec3 size) override;
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapDirtyRegion* outRects,
SizeT maxRects) const override;
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
const void* data, SizeT size) override;
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;