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;