diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 5ce20443..ae32c964 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1065,6 +1065,22 @@ 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. + const Uint64 syncingContentVersion = texture.GetContentVersion(); + const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture); + const Uint32 syncingMipLevelCount = + syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u; + if (outResource.image != VK_NULL_HANDLE && + outResource.syncedContentVersion == syncingContentVersion && + outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() && + outResource.syncedMipLevelCount == syncingMipLevelCount) { + return true; + } + TextureUploadTarget uploadTarget = TextureUploadTarget::Unknown; IntVec3 texelSize{0, 0, 0}; SizeT byteSize = 0; @@ -1111,6 +1127,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { DumpTextureSyncStats(texture.GetExternalIndex(), texture.GetFormat(), uploadTarget, mipLevelCount, texelSize, byteSize, hasDirtyMipLevel); if (!hasDirtyMipLevel) { + outResource.syncedContentVersion = syncingContentVersion; + outResource.syncedMipLevelCount = syncingMipLevelCount; return true; } @@ -1118,6 +1136,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { MGLOG_D("%s: UploadDirtyMipLevels failed", __func__); return false; } + outResource.syncedContentVersion = syncingContentVersion; + outResource.syncedMipLevelCount = syncingMipLevelCount; return true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index a972dc12..7cb8fa0c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -97,6 +97,13 @@ public: VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; Uint16 syncedTextureParamsVersion = 0; + // Snapshot of ITextureObject::GetContentVersion() at the last successful sync; + // lets SyncTexture skip the whole re-check/re-upload when content is unchanged. + Uint64 syncedContentVersion = 0; + // Snapshot of the defined mip-level count at the last sync. Folded into the early-out key + // 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; TextureResource() = default; TextureResource(const TextureResource&) = delete; @@ -120,6 +127,8 @@ public: std::swap(this->viewType, that.viewType); std::swap(this->sampleCount, that.sampleCount); std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion); + std::swap(this->syncedContentVersion, that.syncedContentVersion); + std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); } void Reset() { @@ -166,6 +175,8 @@ public: viewType = VK_IMAGE_VIEW_TYPE_2D; sampleCount = VK_SAMPLE_COUNT_1_BIT; syncedTextureParamsVersion = 0; + syncedContentVersion = 0; + syncedMipLevelCount = 0; } ~TextureResource() { diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index ecb9b2f1..0e790ad4 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -332,6 +332,11 @@ namespace MobileGL::MG_Impl::GLImpl { texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize}); texture.MarkStorageDirty(uploadTarget, level, false); } + // Mip generation grows/regenerates the level set on the GPU without marking any CPU + // level dirty (MarkStorageDirty(...,false) above). Bump the content version so the + // backend re-syncs: a cached sampled VkImageView built for the pre-generate level + // range would otherwise stay stale and clamp LOD>0 sampling to mip 0. + texture.BumpContentVersion(); return true; } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 4aae56a0..a06614c2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -207,12 +207,21 @@ namespace MobileGL { return m_textureParamsVersion; } + Uint64 TextureObjectBase::GetContentVersion() const { + return m_contentVersion; + } + + void TextureObjectBase::BumpContentVersion() { + ++m_contentVersion; + } + Int TextureObjectBase::GetSamples() const { return m_samples; } void TextureObjectBase::SetSamples(Int samples) { m_samples = samples; + ++m_textureParamsVersion; } Bool TextureObjectBase::HasFixedSampleLocations() const { @@ -221,6 +230,7 @@ namespace MobileGL { void TextureObjectBase::SetFixedSampleLocations(Bool fixedSampleLocations) { m_fixedSampleLocations = fixedSampleLocations; + ++m_textureParamsVersion; } Uint64 TextureObjectBase::GetLifetimeId() const { @@ -257,6 +267,9 @@ namespace MobileGL { void TextureObjectWithOneMipmap::MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) { + if (dirty) { + ++m_contentVersion; + } m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty); } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 4b753cd9..a9dc8535 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -47,6 +47,10 @@ namespace MobileGL::MG_State::GLState { virtual Uint GetImmutableLevels() const = 0; virtual void SetImmutableLevels(Uint levels) = 0; virtual Uint16 GetTextureParamsVersion() const = 0; + // Monotonic counter bumped on every CPU-side pixel mutation (see MarkStorageDirty). + // 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; virtual Int GetSamples() const = 0; virtual void SetSamples(Int samples) = 0; virtual Bool HasFixedSampleLocations() const = 0; @@ -86,6 +90,11 @@ namespace MobileGL::MG_State::GLState { Uint GetImmutableLevels() const override; void SetImmutableLevels(Uint levels) override; Uint16 GetTextureParamsVersion() const override; + Uint64 GetContentVersion() 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 + // set changed (so a cached sampled view's level range is stale) but no CPU data is dirty. + void BumpContentVersion(); Int GetSamples() const override; void SetSamples(Int samples) override; Bool HasFixedSampleLocations() const override; @@ -108,6 +117,9 @@ namespace MobileGL::MG_State::GLState { UintVec2 m_levelRange = {0, 1000}; Uint m_immutableLevels = 0; Uint16 m_textureParamsVersion = 0; + // Starts at 1 so a freshly-created backend resource (snapshot 0) never spuriously + // matches before its first sync. Bumped only on dirty=true in MarkStorageDirty. + Uint64 m_contentVersion = 1; Int m_samples = 0; Bool m_fixedSampleLocations = true; }; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index 84385810..cf0b77f2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -41,6 +41,9 @@ namespace MobileGL { } void TextureObject2DCube::MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) { + if (dirty) { + ++m_contentVersion; + } m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty); }