From c2a081fa75e8c073150eb7b14606aa9f35caad5f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 14:02:05 -0400 Subject: [PATCH] [Fix] (GLState, DirectVulkan): bust the texture-sync skip when a re-spec moved only the shape --- .../DirectVulkan/Renderer/VkTextureManager.cpp | 17 +++++++++++++---- .../DirectVulkan/Renderer/VkTextureManager.h | 8 ++++++++ .../GLState/TextureState/TextureObject.cpp | 4 ++++ .../GLState/TextureState/TextureObject.h | 7 +++++++ 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 4c5f7ac7..3fe55663 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1494,6 +1494,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture); const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u; return resource.syncedContentVersion != texture.GetContentVersion() || + resource.syncedShapeVersion != texture.GetShapeVersion() || resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() || resource.syncedMipLevelCount != mipLevelCount; } @@ -1593,11 +1594,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture, TextureResource &outResource) { // Cross-draw fast path: if the resource is already built and neither the texture's - // pixel content (bumped in MarkStorageDirty) nor its params changed since the last - // sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness, - // SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is - // maintained separately by the transition path, so the resource still reflects truth. + // pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion) + // nor its params changed since the last sync, there is nothing to re-check or + // re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and + // the per-level dirty scan. Layout is maintained separately by the transition path, so + // the resource still reflects truth. The shape version is NOT redundant with the + // content one: glTexImage2D(..., nullptr) re-specifies a level's size or format + // without dirtying a texel, which is exactly how a re-specified image-unit texture used + // to keep reporting its old imageSize(). const Uint64 syncingContentVersion = texture.GetContentVersion(); + const Uint64 syncingShapeVersion = texture.GetShapeVersion(); const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture); const Uint32 syncingMipLevelCount = syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u; @@ -1609,6 +1615,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end(); if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && outResource.syncedContentVersion == syncingContentVersion && + outResource.syncedShapeVersion == syncingShapeVersion && outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() && outResource.syncedMipLevelCount == syncingMipLevelCount) { return true; @@ -1660,6 +1667,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (!hasDirtyMipLevel) { outResource.syncedContentVersion = syncingContentVersion; outResource.syncedMipLevelCount = syncingMipLevelCount; + outResource.syncedShapeVersion = syncingShapeVersion; return true; } @@ -1669,6 +1677,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } outResource.syncedContentVersion = syncingContentVersion; outResource.syncedMipLevelCount = syncingMipLevelCount; + outResource.syncedShapeVersion = syncingShapeVersion; return true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 40f23d0d..1b0183df 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -206,6 +206,12 @@ public: // as defense-in-depth: any path that grows the level set (which resizes the sampled view) // busts the skip even if it failed to bump the content version. Uint32 syncedMipLevelCount = 0; + // Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content + // version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an + // already-defined level changes its size or format and dirties no texel, so it moves the + // shape version and nothing else. Without this in the early-out key the image, its views + // and therefore imageSize() all keep answering with the texture's PREVIOUS shape. + Uint64 syncedShapeVersion = 0; TextureResource() = default; TextureResource(const TextureResource&) = delete; @@ -237,6 +243,7 @@ public: std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration); std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); + std::swap(this->syncedShapeVersion, that.syncedShapeVersion); } void Reset() { @@ -300,6 +307,7 @@ public: syncedTextureParamsVersion = 0; syncedContentVersion = 0; syncedMipLevelCount = 0; + syncedShapeVersion = 0; } ~TextureResource() { diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 86c1a4a2..55f0695a 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -250,6 +250,10 @@ namespace MobileGL { return m_contentVersion; } + Uint64 TextureObjectBase::GetShapeVersion() const { + return m_shapeVersion; + } + Bool TextureObjectBase::IsMipmapCompleteForFilterCached(Bool mipmapped) const { const int slot = mipmapped ? 1 : 0; if (m_completeMemoShapeVersion[slot] == m_shapeVersion) { diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 742e5e78..1f214450 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -55,6 +55,12 @@ namespace MobileGL::MG_State::GLState { // Backends compare it against a per-resource snapshot to skip re-syncing unchanged // textures across draws (e.g. the block atlas bound across a whole terrain batch). virtual Uint64 GetContentVersion() const = 0; + // Monotonic counter bumped on every SHAPE mutation - level sizes, the stored level + // set, the internal format, the level range (see BumpShapeVersion). Disjoint from the + // content version on purpose: glTexImage2D(..., nullptr) re-specifies a level's size + // without dirtying a single texel, so a backend that keys its "nothing changed since + // the last sync" skip on content alone keeps a resource of the OLD size alive. + virtual Uint64 GetShapeVersion() const = 0; // Answers IsMipmapCompleteForFilter() from a memo. Sampling completeness is a // property of the texture's SHAPE - level sizes, level count, level range, // internal format - and never of its texel content, but every draw asks about @@ -106,6 +112,7 @@ namespace MobileGL::MG_State::GLState { void SetImmutableLevels(Uint levels) override; Uint16 GetTextureParamsVersion() const override; Uint64 GetContentVersion() const override; + Uint64 GetShapeVersion() const override; Bool IsMipmapCompleteForFilterCached(Bool mipmapped) const override; // Bumps the content version without touching per-level storage-dirty flags. Used when the // set of defined mip levels grows via GPU-side mip generation (glGenerateMipmap): the level