diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index 1e356550..0497edaa 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -11,6 +11,23 @@ namespace MobileGL { namespace MG_State { namespace GLState { + namespace { + // Overlapping OR abutting ([lo, hi) intervals meeting edge-to-edge) in + // every axis: merging abutting boxes keeps scanline/tile write patterns + // as one rect instead of a picket fence. + Bool RegionsTouch(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) { + return a.lo.x() <= b.hi.x() && b.lo.x() <= a.hi.x() && a.lo.y() <= b.hi.y() && + b.lo.y() <= a.hi.y() && a.lo.z() <= b.hi.z() && b.lo.z() <= a.hi.z(); + } + + MipmapDirtyRegion RegionUnion(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) { + return {IntVec3{std::min(a.lo.x(), b.lo.x()), std::min(a.lo.y(), b.lo.y()), + std::min(a.lo.z(), b.lo.z())}, + IntVec3{std::max(a.hi.x(), b.hi.x()), std::max(a.hi.y(), b.hi.y()), + std::max(a.hi.z(), b.hi.z())}}; + } + } // namespace + SizeT MipmapStorage::GetLevelCount() const { return m_data.size(); } @@ -28,6 +45,7 @@ namespace MobileGL { m_texelSizes.resize(requiredLevelCount); m_isDirty.resize(requiredLevelCount, false); m_dirtyRegions.resize(requiredLevelCount); + m_dirtyRects.resize(requiredLevelCount); m_compressedData.resize(requiredLevelCount); m_compressedFormats.resize(requiredLevelCount, GL_NONE); } @@ -44,6 +62,11 @@ namespace MobileGL { std::max(input.texelSize.z(), 1)}} : MipmapDirtyRegion{}; } + // The rect list mirrors the union box's reset: whatever rects were + // pending measured the OLD extents. Empty list = union box tells all. + if (level < m_dirtyRects.size()) { + m_dirtyRects[level].clear(); + } auto& data = m_data[level]; data.resize(input.byteSize, 0); @@ -94,6 +117,7 @@ namespace MobileGL { m_texelSizes.resize(levelCount); m_isDirty.resize(levelCount); m_dirtyRegions.resize(levelCount); + m_dirtyRects.resize(levelCount); m_compressedData.resize(levelCount); m_compressedFormats.resize(levelCount); } @@ -142,6 +166,13 @@ namespace MobileGL { m_dirtyRegions[level] = {}; } } + // Both directions collapse the rect list to "just the union box": a + // whole-level dirty IS the union box, a clean level has nothing to say. + // clear() keeps the vector's capacity, so per-frame streaming levels + // allocate their slots once and reuse them. + if (level < m_dirtyRects.size()) { + m_dirtyRects[level].clear(); + } } bool MipmapStorage::IsDirty(Uint level) const { @@ -158,6 +189,20 @@ namespace MobileGL { std::min(offset.y() + size.y(), levelSize.y()), std::min(offset.z() + std::max(size.z(), 1), std::max(levelSize.z(), 1))}; if (incoming.Empty()) return; + // Rect list first, while the union box still holds only the PREVIOUS + // writes: a level that is already dirty with an empty list is in the + // "union box tells all" resting state, so that box seeds the list + // before the incoming rect refines it. + if (level < m_dirtyRects.size()) { + auto& rects = m_dirtyRects[level]; + if (!m_isDirty[level]) { + rects.clear(); // stale-safety; MarkDirty(false) already cleared it + } else if (rects.empty() && level < m_dirtyRegions.size() && + !m_dirtyRegions[level].Empty()) { + rects.push_back(m_dirtyRegions[level]); + } + InsertDirtyRect(level, incoming); + } if (level < m_dirtyRegions.size()) { MipmapDirtyRegion& region = m_dirtyRegions[level]; if (m_isDirty[level] && !region.Empty()) { @@ -174,10 +219,82 @@ namespace MobileGL { m_isDirty[level] = true; } + void MipmapStorage::InsertDirtyRect(Uint level, MipmapDirtyRegion incoming) { + auto& rects = m_dirtyRects[level]; + if (rects.capacity() < kMaxDirtyRects) { + rects.reserve(kMaxDirtyRects); + } + // Cascade-merge: absorb every rect the incoming touches. The absorbed + // union can reach rects a smaller box did not, so rescan until stable; + // every merge shrinks the list, so this terminates. Swap-with-back keeps + // removal O(1) - the list is unordered by design. + Bool merged = true; + while (merged) { + merged = false; + for (SizeT i = 0; i < rects.size(); ++i) { + if (RegionsTouch(rects[i], incoming)) { + incoming = RegionUnion(rects[i], incoming); + rects[i] = rects.back(); + rects.pop_back(); + merged = true; + break; + } + } + } + if (rects.size() < kMaxDirtyRects) { + rects.push_back(incoming); + return; + } + // Full: fold the incoming rect into the neighbour whose box grows least + // (least new area dragged into the upload), then re-insert the grown + // box - it may now touch others. The removal above guarantees the + // recursion appends on the second pass at the latest. + SizeT best = 0; + SizeT bestGrowth = ~static_cast(0); + for (SizeT i = 0; i < rects.size(); ++i) { + const SizeT growth = RegionUnion(rects[i], incoming).TexelCount() - rects[i].TexelCount(); + if (growth < bestGrowth) { + bestGrowth = growth; + best = i; + } + } + incoming = RegionUnion(rects[best], incoming); + rects[best] = rects.back(); + rects.pop_back(); + InsertDirtyRect(level, incoming); + } + MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const { if (level >= m_dirtyRegions.size()) return {}; return m_dirtyRegions[level]; } + + SizeT MipmapStorage::GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) const { + if (outRects == nullptr || level >= m_dirtyRects.size() || level >= m_dirtyRegions.size()) { + return 0; + } + const auto& rects = m_dirtyRects[level]; + // 0 or 1 rects: the union box already says exactly this. More than the + // caller can take: never truncate - a dropped rect is a dropped write. + if (rects.size() < 2 || rects.size() > maxRects) { + return 0; + } + // Total-bytes accounting: when the scattered rects add up to most of + // the union box anyway (>= 3/4), one driver call on the box beats many + // calls moving nearly the same bytes. + SizeT summedArea = 0; + for (const auto& rect : rects) { + summedArea += rect.TexelCount(); + } + const SizeT unionArea = m_dirtyRegions[level].TexelCount(); + if (summedArea * 4 >= unionArea * 3) { + return 0; + } + for (SizeT i = 0; i < rects.size(); ++i) { + outRects[i] = rects[i]; + } + return rects.size(); + } } // namespace GLState } // namespace MG_State } // namespace MobileGL diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h index f40454bc..fd6b8302 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h @@ -31,6 +31,11 @@ namespace MobileGL { return lo.x() <= 0 && lo.y() <= 0 && lo.z() <= 0 && hi.x() >= levelSize.x() && hi.y() >= levelSize.y() && hi.z() >= std::max(levelSize.z(), 1); } + SizeT TexelCount() const { + if (Empty()) return 0; + return static_cast(hi.x() - lo.x()) * static_cast(hi.y() - lo.y()) * + static_cast(hi.z() - lo.z()); + } }; class MipmapStorage { @@ -54,6 +59,28 @@ namespace MobileGL { // Meaningful only while IsDirty(level). MipmapDirtyRegion GetDirtyRegion(Uint level) const; + // Behind the union box, the level keeps up to kMaxDirtyRects pairwise + // disjoint rects recording WHERE the writes actually landed. A frame of + // ~100 scattered sprite updates in a big atlas has a union box that + // covers nearly the whole level while the touched texels are ~5% of it; + // the union box stays the source of truth (every write funnels through + // MarkDirty/MarkDirtyRegion into BOTH representations), backends OPT IN + // to the list purely as an upload-size refinement. 96 slots because the + // pattern this exists for is Minecraft's ~100 sprites/frame: a 16-slot + // list forced into far-apart merges was measured at >90% of the union + // box's area on exactly that pattern, i.e. worthless. Inserts merge any + // touching/overlapping rect (cascading, so the list stays disjoint); + // when full, the incoming rect folds into the neighbour whose box grows + // least and the list degrades gracefully toward the union box. + static constexpr SizeT kMaxDirtyRects = 96; + // Copies the level's dirty rects into outRects and returns how many were + // written. 0 means "upload the union box instead" and covers every + // reason at once: tracking unavailable, a single rect (identical to the + // union box by construction), more rects than maxRects, or a summed + // area so close to the union box's that one big upload beats many small + // ones (fewer driver calls wins when the bytes are nearly equal). + SizeT GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) const; + // The bytes an application handed to glCompressedTexImage*, kept verbatim beside the // (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires // glGetCompressedTexImage to return the image *as stored*, and no backend here has a @@ -70,10 +97,22 @@ namespace MobileGL { const void* MapCompressedData(Uint level) const; protected: + // Insert one clamped, non-empty write box, keeping the list disjoint + // and bounded (see kMaxDirtyRects). + void InsertDirtyRect(Uint level, MipmapDirtyRegion incoming); + Vector m_texelSizes; Vector> m_data; Vector m_isDirty; Vector m_dirtyRegions; + // Per level, the disjoint rect list behind m_dirtyRegions' union box. + // An EMPTY list is the common resting state and always means "the union + // box is the whole story" - clean levels, whole-level dirties and + // respecifies all just clear it, so plain full-level uploads never pay + // a heap allocation; the first scattered MarkDirtyRegion on an + // already-dirty level seeds the list from the union box accumulated so + // far and refines from there. + Vector> m_dirtyRects; Vector> m_compressedData; Vector m_compressedFormats; }; diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h index 316ad0b2..1f6980d4 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h @@ -84,6 +84,12 @@ namespace MobileGL { return m_storage[targetIndex].GetDirtyRegion(level); } + SizeT GetDirtyRects(Uint targetIndex, Uint level, MipmapDirtyRegion* outRects, + SizeT maxRects) const { + MOBILEGL_ASSERT(targetIndex < TargetCount, "GetDirtyRects: target invalid"); + return m_storage[targetIndex].GetDirtyRects(level, outRects, maxRects); + } + void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data, SizeT size) { MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid"); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index c82c11de..b5bfa3f2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -345,6 +345,13 @@ namespace MobileGL { return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + SizeT TextureObjectWithOneMipmap::GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, + MipmapDirtyRegion* outRects, + SizeT maxRects) const { + return m_textureStorage.GetDirtyRects(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, + outRects, maxRects); + } + void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) { m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index c25ceab1..2bfd1655 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -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; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index 2c8929c1..a1997e4c 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -69,6 +69,12 @@ namespace MobileGL { return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + SizeT TextureObject2DCube::GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, + MipmapDirtyRegion* outRects, SizeT maxRects) const { + return m_textureStorage.GetDirtyRects(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, + outRects, maxRects); + } + void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) { m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h index 3d728c76..547ea0e9 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h @@ -31,6 +31,8 @@ namespace MobileGL { 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;