From 6a02c5fea03d2fce006467b57767e31da5f44225 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Thu, 6 Aug 2026 20:17:19 -0400 Subject: [PATCH] [Perf] (MG_Backend): upload only the rects DirectGLES actually dirtied Consume MipmapStorage's new dirty-rect list: when a level offers a profitable rect list, the sync path issues one glTexSubImage2D/3D per rect under a single UNPACK_ROW_LENGTH set/reset instead of one call covering the union box. Striding is the exact scheme the single-box path already uses (UNPACK_ALIGNMENT pinned to 1 by ScopedDefaultUnpackState, so every bpp is stride-exact); levels without a profitable list take the old path unchanged. On the atlas-streaming case this trades one ~2MB upload for ~95 small ones totalling ~95KB - roughly a wash in driver-call overhead on desktop NVIDIA GL (mc_tex_stream ~-3%), a clear byte-volume win for tiled/mobile GLES where the driver shadow-copies every upload. Unit tests 421/421. --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 54 ++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index ca79a54b..9e1f1a56 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2440,10 +2440,45 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(dirtyRegion.lo.z()) * levelSliceBytes + static_cast(dirtyRegion.lo.y()) * levelRowBytes + static_cast(dirtyRegion.lo.x()) * bpp; + // Scatter refinement behind the union box: ~100 sprite + // writes into an atlas leave a box that spans nearly the + // whole level while the touched texels are a few percent of + // it. The storage's bounded rect list recovers the true + // footprint; each rect is uploaded with the same + // ROW_LENGTH striding into the level shadow as the box + // path. The storage only hands the list out when its + // summed area is materially smaller than the box (0 + // otherwise), so the extra calls always move fewer bytes. + MG_State::GLState::MipmapDirtyRegion + dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; + SizeT dirtyRectCount = 0; + if (subRectEligible) { + dirtyRectCount = textureMipmapObject->GetStorageDirtyRects( + uploadTarget, level, dirtyRects, + MG_State::GLState::MipmapStorage::kMaxDirtyRects); + } + const auto rectShadowPtr = [&](const MG_State::GLState::MipmapDirtyRegion& rect) { + return static_cast(uploadData) + + static_cast(rect.lo.z()) * levelSliceBytes + + static_cast(rect.lo.y()) * levelRowBytes + + static_cast(rect.lo.x()) * bpp; + }; switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: - if (subRectEligible) { + if (subRectEligible && dirtyRectCount >= 2) { + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); + for (SizeT r = 0; r < dirtyRectCount; ++r) { + const auto& rect = dirtyRects[r]; + g_GLESFuncs.glTexSubImage2D( + glUploadTarget, static_cast(level), rect.lo.x(), + rect.lo.y(), static_cast(rect.hi.x() - rect.lo.x()), + static_cast(rect.hi.y() - rect.lo.y()), glFormat, + glType, rectShadowPtr(rect)); + } + // The surrounding ScopedDefaultUnpackState shadow says 0. + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + } else if (subRectEligible) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); g_GLESFuncs.glTexSubImage2D( glUploadTarget, static_cast(level), dirtyRegion.lo.x(), @@ -2463,7 +2498,22 @@ namespace MobileGL::MG_Backend::DirectGLES { // ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly // like a 2D array whose depth is 6 * the cube count. case TextureTarget::TextureCubeMapArray: - if (subRectEligible) { + if (subRectEligible && dirtyRectCount >= 2) { + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y()); + for (SizeT r = 0; r < dirtyRectCount; ++r) { + const auto& rect = dirtyRects[r]; + g_GLESFuncs.glTexSubImage3D( + glUploadTarget, static_cast(level), rect.lo.x(), + rect.lo.y(), rect.lo.z(), + static_cast(rect.hi.x() - rect.lo.x()), + static_cast(rect.hi.y() - rect.lo.y()), + static_cast(rect.hi.z() - rect.lo.z()), glFormat, + glType, rectShadowPtr(rect)); + } + g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + } else if (subRectEligible) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y()); g_GLESFuncs.glTexSubImage3D(