From 9524c688caadef76e6a479dc138e6e8d7b3d5c43 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 29 Sep 2026 09:06:07 -0400 Subject: [PATCH] [Fix] (Espryt, Magma): scattered glTexSubImage writes upload exactly their texels - MipmapStorage keeps an exact dirty footprint beside its rect list (a write log once a merge or a fold would widen the list past the writes), Magma always uploads it, Espryt uploads it box by box (through the unpack ring too) once the driver texture may hold GPU writes and merges it at the image-bindable re-mint; the union box, the 3/4 cut-off, the 96-rect fold and a level-spanning union no longer put the stale shadow over GPU-written texels (two 2x2 writes over a GPU clear: 41 of 64 texels red -> 0) --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 13 ++ MobileGL/MG_Backend/DirectGLES/Managers.cpp | 127 +++++++++---- MobileGL/MG_Backend/DirectGLES/Managers.h | 17 ++ .../Renderer/VkTextureManager.cpp | 42 +++-- .../GLState/TextureState/MipmapStorage.cpp | 174 ++++++++++++++++++ .../GLState/TextureState/MipmapStorage.h | 21 +++ .../TextureState/MipmapUploadTargetArray.h | 5 + .../GLState/TextureState/TextureObject.cpp | 6 + .../GLState/TextureState/TextureObject.h | 14 ++ .../TextureState/TextureObject2DCube.cpp | 5 + .../TextureState/TextureObject2DCube.h | 2 + .../TextureState/TextureObjectView.cpp | 9 + .../GLState/TextureState/TextureObjectView.h | 2 + 13 files changed, 377 insertions(+), 60 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index dbc42c72..d6ee1ace 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1707,6 +1707,11 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } + // Before the sync: `backendTexture` below is a reference into the registry, which the + // lookup inside the note may reshuffle. + if (imageBinding.Access != GL_READ_ONLY) { + NoteDriverSideTextureWrite(imageBinding.Texture); + } auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true); const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget()); const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE; @@ -5919,6 +5924,7 @@ namespace MobileGL::MG_Backend::DirectGLES { continue; } + TextureImpl::NoteDriverSideTextureWrite(destinationTexture); auto backendSource = TextureImpl::SyncTextureObjectToBackend(sourceTexture); auto backendDestination = TextureImpl::SyncTextureObjectToBackend(destinationTexture); if (!backendSource || !backendDestination) continue; @@ -6635,6 +6641,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject((Int)activeTextureUnit) .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)) .GetBoundObject(); + if (textureObject) TextureImpl::NoteDriverSideTextureWrite(textureObject); auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); if (!backendTextureSlot || !*backendTextureSlot) { MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.", @@ -6730,6 +6737,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(activeTextureUnit) .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)) .GetBoundObject(); + if (textureObject) TextureImpl::NoteDriverSideTextureWrite(textureObject); auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); if (!backendTextureSlot || !*backendTextureSlot) { MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.", @@ -6879,6 +6887,8 @@ namespace MobileGL::MG_Backend::DirectGLES { TextureImpl::SyncTextureObjectToBackend(texture); return; } + // Every path below writes the levels above the base on the driver only. + TextureImpl::NoteDriverSideTextureWrite(texture); auto& backendTexture = TextureImpl::SyncTextureObjectToBackend(texture); if (IsDepthOnlyFormat(texture->GetFormat())) { @@ -7152,6 +7162,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // dereference. The frontend validator is what keeps this unreachable and what reports // the error the application is owed; declining is only how a future gap up there stops // being a crash. See the level guard in VulkanRenderer::CopyImageSubData. + if (!dstEndpoint.IsRenderbuffer() && dstEndpoint.Texture) { + TextureImpl::NoteDriverSideTextureWrite(dstEndpoint.Texture); + } if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) || !MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) { MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index f49141cf..82fc9d18 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2787,23 +2787,29 @@ namespace MobileGL::MG_Backend::DirectGLES { } // Replaces one level's shadow bytes with what the driver texture holds for it, then copies - // back the client writes the driver has not been sent yet (the level's pending rects, the - // same boxes a plain sync would upload over the driver's copy). Returns false, leaving the - // shadow untouched, when the driver's copy cannot stand in for the shadow: the client owns - // every byte of the level, the level is stored compressed, the driver has no image at this - // level and extent (defined after the last sync), or the readback cannot be served (a - // multisample image, a format the driver will not attach to a framebuffer). + // back the client writes the driver has not been sent yet - exactly the texels written + // (the level's dirty footprint), not the union box or the rect list, which also span texels + // the client never wrote and which only the driver's copy holds correctly. Returns false, + // leaving the shadow untouched, when the driver's copy cannot stand in for the shadow: the + // client wrote every texel of the level, the level is stored compressed, the driver has no + // image at this level and extent (defined after the last sync), or the readback cannot be + // served (a multisample image, a format the driver will not attach to a framebuffer). static Bool AdoptDriverLevelIntoShadow(GLuint backendTextureId, BackendTextureObject& backendTexture, const MG_State::GLState::ITextureObject& stateTextureObject, MG_State::GLState::TextureObjectMipmap& mipmapObject, TextureUploadTarget uploadTarget, Uint level) { const IntVec3 texelSize = mipmapObject.GetMipmapTexelSize(uploadTarget, level); const IntVec3 extent{texelSize.x(), texelSize.y(), std::max(texelSize.z(), 1)}; - const Bool dirty = mipmapObject.IsStorageDirty(uploadTarget, level); - const MG_State::GLState::MipmapDirtyRegion dirtyRegion = - dirty ? mipmapObject.GetStorageDirtyRegion(uploadTarget, level) - : MG_State::GLState::MipmapDirtyRegion{}; - if (dirty && dirtyRegion.CoversWholeLevel(extent)) return false; + const SizeT texelCount = + static_cast(extent.x()) * static_cast(extent.y()) * static_cast(extent.z()); + Vector pending; + mipmapObject.GetStorageDirtyFootprint(uploadTarget, level, pending); + // Disjoint boxes, so their texels add up; a union box spanning the level is not enough. + SizeT pendingTexels = 0; + for (const auto& box : pending) { + pendingTexels += box.TexelCount(); + } + if (!pending.empty() && pendingTexels >= texelCount) return false; if (mipmapObject.GetMipmapCompressedFormat(uploadTarget, level) != GL_NONE) return false; if (!g_GLESFuncs.glGetTexLevelParameteriv) return false; @@ -2833,28 +2839,18 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } const SizeT shadowBytes = mipmapObject.GetMipmapByteSize(uploadTarget, level); - const SizeT texelCount = - static_cast(extent.x()) * static_cast(extent.y()) * static_cast(extent.z()); auto* shadow = static_cast(mipmapObject.MapMipmapData(uploadTarget, level)); if (shadow == nullptr || snapshot.size() != shadowBytes || texelCount == 0 || shadowBytes % texelCount) { return false; } - if (dirty) { - // The rects index the SHADOW's texel grid (a 1D array's layers are its rows), which + if (!pending.empty()) { + // The boxes index the SHADOW's texel grid (a 1D array's layers are its rows), which // is also the snapshot's byte order. const SizeT bpp = shadowBytes / texelCount; const SizeT rowBytes = static_cast(extent.x()) * bpp; const SizeT sliceBytes = static_cast(extent.y()) * rowBytes; - constexpr SizeT kMaxPending = MG_State::GLState::MipmapStorage::kMaxDirtyRects; - MG_State::GLState::MipmapDirtyRegion pending[kMaxPending]; - SizeT pendingCount = mipmapObject.GetStorageDirtyRects(uploadTarget, level, pending, kMaxPending); - if (pendingCount == 0) { - pending[0] = dirtyRegion; - pendingCount = 1; - } - for (SizeT i = 0; i < pendingCount; ++i) { - const auto& box = pending[i]; + for (const auto& box : pending) { if (box.Empty()) continue; const SizeT spanBytes = static_cast(box.hi.x() - box.lo.x()) * bpp; for (Int z = box.lo.z(); z < box.hi.z(); ++z) { @@ -4340,6 +4336,9 @@ namespace MobileGL::MG_Backend::DirectGLES { ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType); const auto& uploadTargets = textureMipmapObject->GetUploadTargets(); ScopedDefaultUnpackState unpackState; + // Per-level scratch of the exact-footprint path, hoisted to reuse capacity. + Vector dirtyFootprint; + Vector stagingBlockOverflow; for (auto& uploadTarget : uploadTargets) { for (SizeT level = 0; level < mipmapCount; ++level) { if (!textureMipmapObject->IsStorageDirty(uploadTarget, level)) { @@ -4413,13 +4412,16 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT texelCount = static_cast(texelSize.x()) * static_cast(texelSize.y()) * static_cast(std::max(texelSize.z(), 1)); - const Bool subRectEligible = - !dirtyRegion.Empty() && !dirtyRegion.CoversWholeLevel(texelSize) && texelCount > 0 && - uploadBytes > 0 && uploadBytes % texelCount == 0 && + // Whether a box of the level can be addressed at all: whole texels in + // the upload bytes, and an ES image shaped like the shadow. + const Bool regionsAddressable = + !dirtyRegion.Empty() && texelCount > 0 && uploadBytes > 0 && + uploadBytes % texelCount == 0 && (layersAsRows ? std::max(texelSize.z(), 1) == 1 : uploadSize.x() == texelSize.x() && uploadSize.y() == texelSize.y() && std::max(uploadSize.z(), 1) == std::max(texelSize.z(), 1)); - const SizeT bpp = subRectEligible ? uploadBytes / texelCount : 0; + const Bool subRectEligible = regionsAddressable && !dirtyRegion.CoversWholeLevel(texelSize); + const SizeT bpp = regionsAddressable ? uploadBytes / texelCount : 0; const IntVec3 regionSize = {dirtyRegion.hi.x() - dirtyRegion.lo.x(), dirtyRegion.hi.y() - dirtyRegion.lo.y(), dirtyRegion.hi.z() - dirtyRegion.lo.z()}; @@ -4456,6 +4458,27 @@ namespace MobileGL::MG_Backend::DirectGLES { dirtyRectCount = 0; } } + // Everything above - the union box, the ring's one-box rule, the rect + // list with its merges, fold and 3/4 cut-off - may upload shadow texels + // the client did not write, which is only harmless while the driver + // still holds those texels as they were last uploaded. Once it may hold + // GPU writes, the level goes up as exactly the texels written, one box + // (one ring job) each, whatever that costs: more boxes, and a union box + // that spans the whole level no longer means a whole-level upload. + const MG_State::GLState::MipmapDirtyRegion* uploadRects = dirtyRects; + SizeT uploadRectCount = dirtyRectCount; + if (m_driverMayHoldGpuWrites && regionsAddressable) { + textureMipmapObject->GetStorageDirtyFootprint(uploadTarget, level, dirtyFootprint); + // One box is the union box itself, which the branches below already + // upload exactly (as the whole level when it is the whole level). + if (dirtyFootprint.size() >= 2) { + uploadRects = dirtyFootprint.data(); + uploadRectCount = dirtyFootprint.size(); + } else { + uploadRectCount = 0; + } + } + const Bool perRectUpload = regionsAddressable && uploadRectCount >= 2; const auto rectUploadPtr = [&](const MG_State::GLState::MipmapDirtyRegion& rect) { return static_cast(uploadData) + static_cast(rect.lo.z()) * levelSliceBytes + @@ -4491,15 +4514,21 @@ namespace MobileGL::MG_Backend::DirectGLES { // client-pointer path makes the driver walk, which strides over // the whole level width. UnpackStagingBlock - stagingBlocks[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; + stagingBlockArray[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; + UnpackStagingBlock* stagingBlocks = stagingBlockArray; SizeT stagingBlockCount = 0; const Bool ringUsable = BufferImpl::UnpackRingAvailable(); if (!ringUsable) { // Nothing to plan: every branch below keeps the client // pointer and its ROW_LENGTH striding, unchanged. - } else if (subRectEligible && dirtyRectCount >= 2) { - for (SizeT r = 0; r < dirtyRectCount; ++r) { - const auto& rect = dirtyRects[r]; + } else if (perRectUpload) { + // Only an exact footprint outgrows the array (see uploadRects). + if (uploadRectCount > MG_State::GLState::MipmapStorage::kMaxDirtyRects) { + stagingBlockOverflow.resize(uploadRectCount); + stagingBlocks = stagingBlockOverflow.data(); + } + for (SizeT r = 0; r < uploadRectCount; ++r) { + const auto& rect = uploadRects[r]; stagingBlocks[r] = { rectUploadPtr(rect), static_cast(rect.hi.x() - rect.lo.x()) * bpp, @@ -4509,7 +4538,7 @@ namespace MobileGL::MG_Backend::DirectGLES { levelSliceBytes, 0}; } - stagingBlockCount = dirtyRectCount; + stagingBlockCount = uploadRectCount; } else if (subRectEligible) { stagingBlocks[0] = {regionPtr, static_cast(regionSize.x()) * bpp, @@ -4550,10 +4579,10 @@ namespace MobileGL::MG_Backend::DirectGLES { switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) { case TextureTarget::Texture2D: case TextureTarget::TextureCubeMap: - if (subRectEligible && dirtyRectCount >= 2) { + if (perRectUpload) { if (!ringStaged) g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); - for (SizeT r = 0; r < dirtyRectCount; ++r) { - const auto& rect = dirtyRects[r]; + for (SizeT r = 0; r < uploadRectCount; ++r) { + const auto& rect = uploadRects[r]; g_GLESFuncs.glTexSubImage2D( glUploadTarget, static_cast(level), rect.lo.x(), rect.lo.y(), static_cast(rect.hi.x() - rect.lo.x()), @@ -4589,18 +4618,18 @@ 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 && dirtyRectCount >= 2) { + if (perRectUpload) { if (!ringStaged) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x()); g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, imageHeight); } - for (SizeT r = 0; r < dirtyRectCount; ++r) { - const auto box = ToBackendBox(dirtyRects[r]); + for (SizeT r = 0; r < uploadRectCount; ++r) { + const auto box = ToBackendBox(uploadRects[r]); g_GLESFuncs.glTexSubImage3D( glUploadTarget, static_cast(level), box.x, box.y, box.z, box.w, box.h, box.d, glFormat, glType, ringStaged ? UnpackRingPixelOffset(stagingBlocks[r].offset) - : static_cast(rectUploadPtr(dirtyRects[r]))); + : static_cast(rectUploadPtr(uploadRects[r]))); } if (!ringStaged) { g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); @@ -5147,6 +5176,21 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundTexturesCache; StateBackendObjectRegistry g_backendTextureObjects; + + void NoteDriverSideTextureWrite(const SharedPtr& textureObject) { + SharedPtr storage = textureObject; + // Bounded: GL resolves a view of a view to the original storage, so this takes one step. + for (Uint hop = 0; storage && storage->IsTextureView() && hop < 4; ++hop) { + storage = storage->GetViewStorageOwner(); + } + if (!storage) return; + auto* slot = g_backendTextureObjects.Find(storage.get()); + auto& backendObj = slot ? *slot : g_backendTextureObjects.GetOrCreate(storage); + if (!backendObj) { + backendObj = MakeShared(); + } + backendObj->NoteDriverSideWrite(); + } } // namespace TextureImpl namespace FramebufferImpl { @@ -5307,6 +5351,9 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } backendTextureObject->SyncMipmapsToBackend(textureObject); + // From here on every draw, clear and blit into this framebuffer can write the + // texture behind the shadow's back. + TextureImpl::NoteDriverSideTextureWrite(textureObject); if (attachmentObject.IsLayered()) { g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment, backendTextureObject->GetBackendTextureId(), diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 6603f2cc..8aa931e7 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -976,6 +976,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // carrier the storage is an integer texture holding codes and glGetTexImage still owes // the application floats. Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; } + // See m_driverMayHoldGpuWrites. Callers holding the STATE texture use + // NoteDriverSideTextureWrite, which also reaches the storage behind a view. + void NoteDriverSideWrite() { m_driverMayHoldGpuWrites = true; } void Bind(GLenum target, Uint unit = TempTextureUnit); Uint GetBackendTextureId() const; @@ -1059,6 +1062,15 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool m_isInitialized = false; Bool m_imageBindableStorageRequired = false; Bool m_backendStorageImmutable = false; + // Whether the driver texture may hold texels the shadow never saw. Set the first time + // anything other than a texel upload can write it - an application framebuffer + // attachment, a writable image binding, a copy or blit into it, a driver-side mipmap + // generation - and never cleared. Until then the driver holds, outside a level's + // pending writes, exactly the shadow bytes it was last sent, so those writes may go up + // as their union box (one unpack-ring job, which is what Mali needs); from then on + // only the texels the client wrote may go up, since the shadow is stale everywhere + // else. See SyncMipmapsToBackend. + Bool m_driverMayHoldGpuWrites = false; // Latches the "this driver has no buffer textures" report to once per texture. The // report is emitted from the respecify path, which bails before recording the state // it was asked to apply - so without the latch the texture stays permanently dirty @@ -1126,6 +1138,11 @@ namespace MobileGL::MG_Backend::DirectGLES { SharedPtr& SyncTextureObjectToBackend( const SharedPtr& textureObject, Bool imageBindableStorageRequired = false); + // Every path that lets the DRIVER write a texture's texels calls this (see + // BackendTextureObject::m_driverMayHoldGpuWrites): an application framebuffer attachment, + // a writable image binding, a copy, blit or mipmap generation into it. A view writes its + // storage texture's image, so the storage texture is the one marked. + void NoteDriverSideTextureWrite(const SharedPtr& textureObject); // Brings every texture the next draw reads - the touched units' bindings and the draw // FBO's texture attachments - onto the backend, through the two borrowed-pair memos // documented at their definitions. Declared here so tests can drive those memos directly. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index c00b0752..309a274e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -2879,11 +2879,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { IntVec3 regionLo = {0, 0, 0}; IntVec3 regionSize = {0, 0, 0}; SizeT texelBytes = 0; - // Scatter refinement of the single dirty box: when the storage's rect - // list reports the writes' true footprint (~100 sprites whose union box - // spans the whole atlas), each rect is staged tightly and copied with - // its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage. Empty means - // "stage the one box above". Only set while subRegion. + // The writes' exact footprint when it is more than one box (~100 sprites + // whose union box spans the whole atlas): each rect is staged tightly and + // copied with its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage. + // Empty means "stage the one box above". Only set while subRegion. Vector rects; }; @@ -2895,6 +2894,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { targets.push_back(uploadTarget); } const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(mipmapTexture.GetFormat()); + Vector dirtyFootprint; VkDeviceSize stagingSize = 0; for (const TextureUploadTarget target : targets) { @@ -2956,8 +2956,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { const SizeT texelCount = static_cast(texelSize.x()) * static_cast(texelSize.y()) * static_cast(std::max(texelSize.z(), 1)); - if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 && - levelBytes % texelCount == 0) { + // Exactly the texels the client wrote, never their union box or the storage's + // rect list (which merges touching writes into their bounding box, folds when + // full and is withheld past 3/4 of the box): the image may hold what the GPU + // drew or cleared anywhere else, and the shadow is stale there. Each box is one + // more VkBufferImageCopy in the same vkCmdCopyBufferToImage, which costs next + // to nothing, so there is no cheaper shape worth being wrong for. A union box + // that spans the level with gaps in it is not a whole-level write either. + mipmapTexture.GetStorageDirtyFootprint(target, level, dirtyFootprint); + if (!region.Empty() && (dirtyFootprint.size() >= 2 || !region.CoversWholeLevel(texelSize)) && + texelCount > 0 && levelBytes % texelCount == 0) { uploadItem.subRegion = true; uploadItem.regionLo = region.lo; uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(), @@ -2967,17 +2975,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { static_cast(uploadItem.regionSize.y()) * static_cast(uploadItem.regionSize.z()) * uploadItem.texelBytes; - // Scatter refinement: the storage only hands out its rect list - // when the rects' summed area is materially smaller than the - // union box (0 otherwise), so taking it always stages fewer - // bytes than the box - the very amplification this path exists - // to avoid paying twice. - MG_State::GLState::MipmapDirtyRegion - dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; - const SizeT dirtyRectCount = mipmapTexture.GetStorageDirtyRects( - target, level, dirtyRects, MG_State::GLState::MipmapStorage::kMaxDirtyRects); - if (dirtyRectCount >= 2) { - uploadItem.rects.assign(dirtyRects, dirtyRects + dirtyRectCount); + // Several boxes: each is staged tightly and copied on its own. One box is + // the union box above. + if (dirtyFootprint.size() >= 2) { + uploadItem.rects.assign(dirtyFootprint.begin(), dirtyFootprint.end()); SizeT rectTexels = 0; for (const auto& rect : uploadItem.rects) { rectTexels += rect.TexelCount(); @@ -3211,6 +3212,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const Bool depthSelectsArrayLayer = outResource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY || outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY || outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + Vector rectCopies; for (const auto& item : uploadItems) { if (!item.rects.empty()) { // Multi-rect item: one VkBufferImageCopy per rect, all submitted in a @@ -3218,7 +3220,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // construction, so no two copies write the same texels. Multi-rect // implies subRegion, which implies a plain color aspect - the combined // depth-stencil split below can never see one of these. - VkBufferImageCopy rectCopies[MG_State::GLState::MipmapStorage::kMaxDirtyRects]; + rectCopies.resize(item.rects.size()); Uint32 rectCopyCount = 0; VkDeviceSize runningOffset = item.offset; for (const auto& rect : item.rects) { @@ -3249,7 +3251,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { runningOffset += static_cast(rect.TexelCount() * item.texelBytes); } vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies); + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies.data()); continue; } const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast(item.texelSize.z()) : 1u; diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index 58affe99..3ba2ecdf 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -8,6 +8,9 @@ #include "MipmapStorage.h" +#include +#include + namespace MobileGL { namespace MG_State { namespace GLState { @@ -26,6 +29,135 @@ namespace MobileGL { 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())}}; } + + MipmapDirtyRegion RegionIntersection(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) { + return {IntVec3{std::max(a.lo.x(), b.lo.x()), std::max(a.lo.y(), b.lo.y()), + std::max(a.lo.z(), b.lo.z())}, + IntVec3{std::min(a.hi.x(), b.hi.x()), std::min(a.hi.y(), b.hi.y()), + std::min(a.hi.z(), b.hi.z())}}; + } + + // For two boxes of written texels: whether their bounding box holds no texel + // outside them, i.e. whether merging them keeps the rect list exact. + Bool UnionIsExact(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) { + return RegionUnion(a, b).TexelCount() == + a.TexelCount() + b.TexelCount() - RegionIntersection(a, b).TexelCount(); + } + + Bool SameFootprintXY(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) { + return a.lo.x() == b.lo.x() && a.hi.x() == b.hi.x() && a.lo.y() == b.lo.y() && + a.hi.y() == b.hi.y(); + } + + Bool FootprintXYLess(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) { + if (a.lo.y() != b.lo.y()) return a.lo.y() < b.lo.y(); + if (a.lo.x() != b.lo.x()) return a.lo.x() < b.lo.x(); + if (a.hi.y() != b.hi.y()) return a.hi.y() < b.hi.y(); + return a.hi.x() < b.hi.x(); + } + + // Cuts the union of a set of (possibly overlapping) write boxes into disjoint boxes + // holding exactly its texels, at a cost set by the rows written, not by the level. + // Every box is split into row runs, the runs of each row are sorted and merged, and + // a sweep down each slice lets a row's run continue the box above it when it spans + // the same columns on the very next row; a slice's box then continues the previous + // slice's when that slice is the one before and the box covers the same rows and + // columns. + void DecomposeFootprintWrites(const Vector& writes, Vector& out) { + struct Run { + Int z, y, x0, x1; + }; + Vector runs; + for (const auto& box : writes) { + for (Int z = box.lo.z(); z < box.hi.z(); ++z) + for (Int y = box.lo.y(); y < box.hi.y(); ++y) runs.push_back({z, y, box.lo.x(), box.hi.x()}); + } + std::sort(runs.begin(), runs.end(), [](const Run& a, const Run& b) { + if (a.z != b.z) return a.z < b.z; + if (a.y != b.y) return a.y < b.y; + return a.x0 < b.x0; + }); + SizeT merged = 0; + for (const Run& run : runs) { + Run* last = merged > 0 ? &runs[merged - 1] : nullptr; + if (last && last->z == run.z && last->y == run.y && run.x0 <= last->x1) { + last->x1 = std::max(last->x1, run.x1); + } else { + runs[merged++] = run; + } + } + runs.resize(merged); + + Vector open, next, slice; + Vector previousSlice, currentSlice; // indices into `out`, in FootprintXYLess order + Int sliceZ = 0, previousZ = 0, rowY = 0; + Bool haveSlice = false; + // Ends every open box below row `rowY` and folds the slice into `out`. + const auto closeSlice = [&]() { + for (auto& box : open) { + box.hi = {box.hi.x(), rowY + 1, sliceZ + 1}; + slice.push_back(box); + } + open.clear(); + std::sort(slice.begin(), slice.end(), FootprintXYLess); + if (previousZ + 1 != sliceZ) previousSlice.clear(); + currentSlice.clear(); + SizeT p = 0; + for (const auto& box : slice) { + while (p < previousSlice.size() && FootprintXYLess(out[previousSlice[p]], box)) ++p; + if (p < previousSlice.size() && SameFootprintXY(out[previousSlice[p]], box)) { + out[previousSlice[p]].hi = {box.hi.x(), box.hi.y(), sliceZ + 1}; + currentSlice.push_back(previousSlice[p++]); + } else { + currentSlice.push_back(out.size()); + out.push_back(box); + } + } + std::swap(previousSlice, currentSlice); + slice.clear(); + previousZ = sliceZ; + }; + for (SizeT i = 0; i < runs.size();) { + const Int z = runs[i].z; + const Int y = runs[i].y; + if (haveSlice && z != sliceZ) { + closeSlice(); + } else if (haveSlice && y != rowY + 1) { + // A row with no runs in between: nothing open continues across it. + for (auto& box : open) { + box.hi = {box.hi.x(), rowY + 1, sliceZ + 1}; + slice.push_back(box); + } + open.clear(); + } + haveSlice = true; + sliceZ = z; + next.clear(); + SizeT o = 0; + for (; i < runs.size() && runs[i].z == z && runs[i].y == y; ++i) { + const Int x0 = runs[i].x0; + const Int x1 = runs[i].x1; + // `open` is sorted by x and disjoint: every box left of this run that + // it cannot continue ends on the row above. + while (o < open.size() && open[o].lo.x() < x0) { + open[o].hi = {open[o].hi.x(), y, z + 1}; + slice.push_back(open[o++]); + } + if (o < open.size() && open[o].lo.x() == x0 && open[o].hi.x() == x1) { + next.push_back(open[o++]); + } else { + next.push_back({IntVec3{x0, y, z}, IntVec3{x1, y + 1, z + 1}}); + } + } + for (; o < open.size(); ++o) { + open[o].hi = {open[o].hi.x(), y, z + 1}; + slice.push_back(open[o]); + } + std::swap(open, next); + rowY = y; + } + if (haveSlice) closeSlice(); + } } // namespace SizeT MipmapStorage::GetLevelCount() const { @@ -46,6 +178,7 @@ namespace MobileGL { m_isDirty.resize(requiredLevelCount, false); m_dirtyRegions.resize(requiredLevelCount); m_dirtyRects.resize(requiredLevelCount); + m_footprintWrites.resize(requiredLevelCount); m_compressedData.resize(requiredLevelCount); m_compressedFormats.resize(requiredLevelCount, GL_NONE); m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE); @@ -68,6 +201,9 @@ namespace MobileGL { if (level < m_dirtyRects.size()) { m_dirtyRects[level].clear(); } + if (level < m_footprintWrites.size()) { + m_footprintWrites[level].clear(); + } auto& data = m_data[level]; data.resize(input.byteSize, 0); @@ -132,6 +268,7 @@ namespace MobileGL { m_isDirty.resize(levelCount); m_dirtyRegions.resize(levelCount); m_dirtyRects.resize(levelCount); + m_footprintWrites.resize(levelCount); m_compressedData.resize(levelCount); m_compressedFormats.resize(levelCount); m_requestedCompressedFormats.resize(levelCount); @@ -188,6 +325,9 @@ namespace MobileGL { if (level < m_dirtyRects.size()) { m_dirtyRects[level].clear(); } + if (level < m_footprintWrites.size()) { + m_footprintWrites[level].clear(); + } } bool MipmapStorage::IsDirty(Uint level) const { @@ -212,11 +352,17 @@ namespace MobileGL { auto& rects = m_dirtyRects[level]; if (!m_isDirty[level]) { rects.clear(); // stale-safety; MarkDirty(false) already cleared it + m_footprintWrites[level].clear(); } else if (rects.empty() && level < m_dirtyRegions.size() && !m_dirtyRegions[level].Empty()) { rects.push_back(m_dirtyRegions[level]); } + // Entering the log inside the insert already recorded this write. + const Bool logged = !m_footprintWrites[level].empty(); InsertDirtyRect(level, incoming); + if (logged) { + m_footprintWrites[level].push_back(incoming); + } } if (level < m_dirtyRegions.size()) { MipmapDirtyRegion& region = m_dirtyRegions[level]; @@ -248,6 +394,9 @@ namespace MobileGL { merged = false; for (SizeT i = 0; i < rects.size(); ++i) { if (RegionsTouch(rects[i], incoming)) { + if (!UnionIsExact(rects[i], incoming)) { + EnterFootprintLog(level, incoming); + } incoming = RegionUnion(rects[i], incoming); rects[i] = rects.back(); rects.pop_back(); @@ -260,6 +409,7 @@ namespace MobileGL { rects.push_back(incoming); return; } + EnterFootprintLog(level, incoming); // 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 @@ -310,6 +460,30 @@ namespace MobileGL { } return rects.size(); } + + void MipmapStorage::EnterFootprintLog(Uint level, const MipmapDirtyRegion& pending) { + auto& log = m_footprintWrites[level]; + if (!log.empty()) return; + log.assign(m_dirtyRects[level].begin(), m_dirtyRects[level].end()); + log.push_back(pending); + } + + void MipmapStorage::GetDirtyFootprint(Uint level, Vector& outRects) const { + outRects.clear(); + if (level >= m_isDirty.size() || !m_isDirty[level] || level >= m_dirtyRegions.size()) return; + if (level < m_footprintWrites.size() && !m_footprintWrites[level].empty()) { + DecomposeFootprintWrites(m_footprintWrites[level], outRects); + return; + } + // No log: every merge so far was exact and nothing folded, so the list is the + // footprint - and an empty list leaves the union box, which is then a whole-level + // write (MarkDirty, a respecify) or the single box one write left. + if (level < m_dirtyRects.size() && !m_dirtyRects[level].empty()) { + outRects.assign(m_dirtyRects[level].begin(), m_dirtyRects[level].end()); + } else if (!m_dirtyRegions[level].Empty()) { + outRects.push_back(m_dirtyRegions[level]); + } + } } // 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 fe632db4..6774c07e 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h @@ -80,6 +80,15 @@ namespace MobileGL { // 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 level's pending writes EXACTLY: pairwise-disjoint boxes holding every texel + // written since the level was last clean and no other, however many there are and + // however much of the union box they fill. Empty while the level is clean. The + // rect list above only COVERS the writes - a merge replaces two touching rects by + // their bounding box and a full list folds - which is harmless while the texels + // outside the writes are the same in the shadow and in the backend's copy, and + // wrong once the backend's copy may hold texels the GPU wrote: the shadow is stale + // there, and anything uploaded beyond the writes goes up over them. + void GetDirtyFootprint(Uint level, Vector& outRects) 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 @@ -112,6 +121,10 @@ namespace MobileGL { // Insert one clamped, non-empty write box, keeping the list disjoint // and bounded (see kMaxDirtyRects). void InsertDirtyRect(Uint level, MipmapDirtyRegion incoming); + // Starts the level's footprint log (see m_footprintWrites) the moment the rect list + // is about to stop being exact. `pending` is the write being inserted, merged with + // the rects it has absorbed so far - still exact, and no longer in the list. + void EnterFootprintLog(Uint level, const MipmapDirtyRegion& pending); Vector m_texelSizes; Vector> m_data; @@ -125,6 +138,14 @@ namespace MobileGL { // already-dirty level seeds the list from the union box accumulated so // far and refines from there. Vector> m_dirtyRects; + // Per level, the boxes whose union is exactly the pending writes' footprint, as they + // came (overlaps and all; GetDirtyFootprint cuts them apart). EMPTY is the resting + // state and means the rect list (or, while that is empty, the union box) is itself + // exact; the log starts only at the first inexact merge or fold, from the list as it + // stood just before, and then takes every write verbatim until the level is clean or + // respecified - one box per write, so its cost follows the writes and never the + // level's size. clear() keeps the capacity, like the rect list's. + Vector> m_footprintWrites; Vector> m_compressedData; Vector m_compressedFormats; Vector m_requestedCompressedFormats; diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h index 5142422c..d1ec8f5d 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h @@ -90,6 +90,11 @@ namespace MobileGL { return m_storage[targetIndex].GetDirtyRects(level, outRects, maxRects); } + void GetDirtyFootprint(Uint targetIndex, Uint level, Vector& outRects) const { + MOBILEGL_ASSERT(targetIndex < TargetCount, "GetDirtyFootprint: target invalid"); + m_storage[targetIndex].GetDirtyFootprint(level, outRects); + } + 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 039e301e..4522cb79 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -379,6 +379,12 @@ namespace MobileGL { outRects, maxRects); } + void TextureObjectWithOneMipmap::GetStorageDirtyFootprint(TextureUploadTarget uploadTarget, + Uint mipmapLevel, + Vector& outRects) const { + m_textureStorage.GetDirtyFootprint(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, outRects); + } + 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 48ef2777..80cab6e2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -269,6 +269,18 @@ namespace MobileGL::MG_State::GLState { (void)maxRects; return 0; } + // Exactly the texels written since the level was last clean, as disjoint boxes - not a + // cover of them like the rects above, and never withheld. See MipmapStorage:: + // GetDirtyFootprint for why a backend whose copy of the level may hold GPU-written texels + // uploads these and nothing else. The base fallback answers the whole-level box its + // MarkStorageDirtyRegion recorded. + virtual void GetStorageDirtyFootprint(TextureUploadTarget uploadTarget, Uint mipmapLevel, + Vector& outRects) const { + outRects.clear(); + if (IsStorageDirty(uploadTarget, mipmapLevel)) { + outRects.push_back(GetStorageDirtyRegion(uploadTarget, mipmapLevel)); + } + } // 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 @@ -351,6 +363,8 @@ namespace MobileGL::MG_State::GLState { MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapDirtyRegion* outRects, SizeT maxRects) const override; + void GetStorageDirtyFootprint(TextureUploadTarget uploadTarget, Uint mipmapLevel, + Vector& outRects) 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 765477c5..3f2c9338 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -75,6 +75,11 @@ namespace MobileGL { outRects, maxRects); } + void TextureObject2DCube::GetStorageDirtyFootprint(TextureUploadTarget uploadTarget, Uint mipmapLevel, + Vector& outRects) const { + m_textureStorage.GetDirtyFootprint(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, outRects); + } + 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 d705db02..546d6a04 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h @@ -33,6 +33,8 @@ namespace MobileGL { Uint mipmapLevel) const override; SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapDirtyRegion* outRects, SizeT maxRects) const override; + void GetStorageDirtyFootprint(TextureUploadTarget uploadTarget, Uint mipmapLevel, + Vector& outRects) 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/TextureObjectView.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp index c299c1c2..cf02f662 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp @@ -322,6 +322,15 @@ namespace MobileGL::MG_State::GLState { return m_ownerMipmap->GetStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel)); } + // In the owner's texel space, like GetStorageDirtyRegion. + void TextureObjectView::GetStorageDirtyFootprint(TextureUploadTarget uploadTarget, Uint mipmapLevel, + Vector& outRects) const { + outRects.clear(); + if (m_ownerMipmap == nullptr) return; + m_ownerMipmap->GetStorageDirtyFootprint(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), + outRects); + } + void TextureObjectView::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) { if (m_ownerMipmap == nullptr) return; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObjectView.h b/MobileGL/MG_State/GLState/TextureState/TextureObjectView.h index 9a4d9ecb..93b3b1c2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObjectView.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObjectView.h @@ -82,6 +82,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; + void GetStorageDirtyFootprint(TextureUploadTarget uploadTarget, Uint mipmapLevel, + Vector& outRects) const override; void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat, const void* data, SizeT size) override; GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;