From e724e88eece6904122b6eee870f22839a82852d5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 20 Jul 2026 01:43:52 -0400 Subject: [PATCH] [Fix] (MG_State, MG_Impl/GLImpl): allocating a mipmap level no longer truncates the chain above it - AllocateLevel now only grows and the callers that genuinely redefine the whole level set (glTexStorage*, mip regeneration, multisample storage, level-0 respecification) drop the tail explicitly --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 51 +++++++++++ .../GLState/TextureState/MipmapStorage.cpp | 28 ++++-- .../GLState/TextureState/MipmapStorage.h | 4 + .../TextureState/MipmapUploadTargetArray.h | 8 ++ .../GLState/TextureState/TextureObject.cpp | 4 + .../GLState/TextureState/TextureObject.h | 5 ++ .../TextureState/TextureObject2DCube.cpp | 4 + .../TextureState/TextureObject2DCube.h | 1 + MobileGL/MG_Test/Texture/TextureTest.cpp | 89 +++++++++++++++++++ 9 files changed, 188 insertions(+), 6 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 522729e9..8442938a 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -350,6 +350,10 @@ namespace MobileGL::MG_Impl::GLImpl { texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize}); texture.MarkStorageDirty(uploadTarget, level, false); } + // glGenerateMipmap defines exactly levels 0..requiredLevelCount-1. AllocateStorage only + // grows, so a previously longer chain (a bigger base image before respecification) would + // otherwise keep a tail of stale levels here and read as incomplete. + texture.TruncateMipmapLevels(uploadTarget, requiredLevelCount); // 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 @@ -456,9 +460,46 @@ namespace MobileGL::MG_Impl::GLImpl { textureObject->SetSamples(samples); textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE); textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0}); + // Multisample textures are single-level by definition, so a name that previously held a + // mip chain must not keep its tail now that AllocateStorage only grows. + textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 1); textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false); } + // Redefining level 0 of a texture that already had a base image drops the rest of the chain, + // which is exactly what AllocateLevel used to do implicitly for every level. Keeping that + // behaviour for level 0 - and only for level 0 - is what makes the grow-only change safe: + // any level-0 respecification leaves the chain in precisely the state it would have had + // before, while an upload to level N no longer destroys the levels beneath it. + // + // Why it has to be *every* level-0 respecification and not just a size change: Minecraft's + // Mipmap Levels setting rebuilds the block atlas at the SAME dimensions with a different + // level count. A size-only test would leave the old tail in place, and because Mojang + // terminates its chains with a 0x0 level the result is the zero-then-nonzero pattern that + // IsComplete() rejects (TextureObject.cpp) - whereupon DirectGLES skips syncing the texture + // entirely (Managers.cpp) and the atlas samples black. + // + // The "already has a base image" test is what lets the fix work at all: a level that was + // never written reads back as {0,0,0}, so building a chain top-down - upload level N first, + // then level 0 - must not discard the levels just uploaded. That ordering is what + // KHR-GL33.texture_repeat_mode does. + // Scoped to the respecified upload target only, which is what AllocateLevel already did. + // Cube maps keep six independent chains while reporting a single level count (face +X), so + // respecifying a face other than +X can leave the count longer than that face - but that + // asymmetry predates this change and widening the truncation to all six faces would destroy + // mip data for faces the application never touched. Left alone deliberately. + void DiscardMipmapChainOnBaseRespecification(MG_State::GLState::TextureObjectMipmap* texture, + TextureUploadTarget uploadTarget, Uint level) { + if (level != 0) return; + + const IntVec3 existingBaseSize = texture->GetMipmapTexelSize(uploadTarget, 0); + const Bool hasExistingBaseImage = + existingBaseSize.x() > 0 && existingBaseSize.y() > 0 && existingBaseSize.z() > 0; + if (!hasExistingBaseImage) return; + + texture->TruncateMipmapLevels(uploadTarget, 1); + } + // Compressed texture upload is not implemented yet. GL_NUM_COMPRESSED_TEXTURE_FORMATS // reports 0, so every compressed internalformat is by definition unsupported and // GL_INVALID_ENUM is the specified error - unlike THROW_UNIMPL_EXCEPTION, which unwinds @@ -1740,6 +1781,7 @@ namespace MobileGL::MG_Impl::GLImpl { if (isProxy) { MGLOG_D("%s: isProxy = true, not allocating", __func__); } else { + DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes}); } @@ -1867,6 +1909,7 @@ namespace MobileGL::MG_Impl::GLImpl { MGLOG_D("%s: isProxy = true, not allocating", __func__); } else { MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level); + DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes}); } @@ -1955,6 +1998,7 @@ namespace MobileGL::MG_Impl::GLImpl { "Texture object here should always be an object with mipmap"); auto textureMipmapObject = static_cast(textureObject.get()); if (!isProxy) { + DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes}); } @@ -3195,6 +3239,9 @@ namespace MobileGL::MG_Impl::GLImpl { textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize}); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); } + // Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a + // longer pre-existing chain has to be dropped explicitly. + textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast(levels)); textureObject->SetImmutableLevels(static_cast(levels)); } @@ -3247,6 +3294,8 @@ namespace MobileGL::MG_Impl::GLImpl { textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize}); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); } + // See TextureStorage1D. + textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast(levels)); textureObject->SetImmutableLevels(static_cast(levels)); } @@ -3299,6 +3348,8 @@ namespace MobileGL::MG_Impl::GLImpl { {{levelWidth, levelHeight, levelDepth}, byteSize}); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); } + // See TextureStorage1D. + textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast(levels)); textureObject->SetImmutableLevels(static_cast(levels)); } diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index e4127398..93f94675 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -16,17 +16,32 @@ namespace MobileGL { } void MipmapStorage::AllocateLevel(Uint level, MipmapInput input) { - m_data.reserve(std::bit_ceil(level + 1)); - m_data.resize(level + 1); - m_texelSizes.reserve(std::bit_ceil(level + 1)); - m_texelSizes.resize(level + 1); - m_texelSizes[level] = input.texelSize; - m_isDirty.resize(level + 1, false); + // Grow only. GL respecifies exactly the level it is handed, so allocating level 0 + // must not disturb the levels above it - but resize() shrinks as readily as it + // grows, so this used to truncate the whole chain to a single level. Callers that + // genuinely redefine the complete level set say so with TruncateToLevelCount. + const SizeT requiredLevelCount = static_cast(level) + 1; + if (m_data.size() < requiredLevelCount) { + m_data.reserve(std::bit_ceil(requiredLevelCount)); + m_data.resize(requiredLevelCount); + m_texelSizes.reserve(std::bit_ceil(requiredLevelCount)); + m_texelSizes.resize(requiredLevelCount); + m_isDirty.resize(requiredLevelCount, false); + } + m_texelSizes[level] = input.texelSize; auto& data = m_data[level]; data.resize(input.byteSize, 0); } + void MipmapStorage::TruncateToLevelCount(SizeT levelCount) { + if (levelCount >= m_data.size()) return; + + m_data.resize(levelCount); + m_texelSizes.resize(levelCount); + m_isDirty.resize(levelCount); + } + void MipmapStorage::UpdateSubData(Uint level, DataPtr input) { auto& targetData = m_data; MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range"); @@ -55,6 +70,7 @@ namespace MobileGL { } SizeT MipmapStorage::GetByteSize(Uint level) const { + if (level >= m_data.size()) return 0; return m_data[level].size(); } diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h index 5976986d..61c1bc9d 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h @@ -19,6 +19,10 @@ namespace MobileGL { public: SizeT GetLevelCount() const; void AllocateLevel(Uint level, MipmapInput input); + // Discard every level at or above levelCount. AllocateLevel never shrinks, so this + // is the only way a chain gets shorter - use it where the caller defines the whole + // level set (glTexStorage*, mip regeneration, atlas respecification). + void TruncateToLevelCount(SizeT levelCount); void UpdateSubData(Uint level, DataPtr input); void* MapData(Uint level); IntVec3 GetTexelSize(Uint level) const; diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h index c0fc19dd..9e452271 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h @@ -29,6 +29,14 @@ namespace MobileGL { m_storage[targetIndex].AllocateLevel(level, input); } + // Per-target, like AllocateLevel: cube-map faces are respecified independently, so + // truncating one face must not disturb the others. + void TruncateToLevelCount(Uint targetIndex, SizeT levelCount) { + MOBILEGL_ASSERT(targetIndex < TargetCount, "TruncateToLevelCount: target invalid"); + + m_storage[targetIndex].TruncateToLevelCount(levelCount); + } + void UpdateSubData(Uint targetIndex, Uint level, DataPtr input) { MOBILEGL_ASSERT(targetIndex < TargetCount, "UpdateSubData: target invalid"); m_storage[targetIndex].UpdateSubData(level, input); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index ee095cb4..fb3e6f74 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -271,6 +271,10 @@ namespace MobileGL { m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input); } + void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) { + m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount); + } + void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) { m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index a5f3a21f..6bf9b9e7 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -134,6 +134,10 @@ namespace MobileGL::MG_State::GLState { virtual const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const = 0; virtual const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const = 0; virtual void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) = 0; + // AllocateStorage only ever grows the chain. Callers that define the complete level set - + // glTexStorage*, mip regeneration, or a level-0 respecification at a new size - drop the + // leftovers explicitly, so a stale tail can never make the texture silently incomplete. + virtual void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) = 0; virtual void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) = 0; virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0; virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0; @@ -175,6 +179,7 @@ namespace MobileGL::MG_State::GLState { const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override; const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override; void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override; + void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override; void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override; void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override; void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index cf0b77f2..9d6b0e66 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -31,6 +31,10 @@ namespace MobileGL { m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input); } + void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) { + m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount); + } + void TextureObject2DCube::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) { m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h index 2d97a8b2..e1f2f5ce 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h @@ -22,6 +22,7 @@ namespace MobileGL { const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override; const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override; void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override; + void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override; void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override; void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override; void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override; diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index b8154c1a..9a6d58e2 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -1427,6 +1427,95 @@ TEST_F(TextureTest, TextureStorage1DAndSubImageModifyNamedObjectOnly) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// Building a mip chain top-down - upload level N, then level 0 - must not destroy the levels +// already uploaded. AllocateLevel used to resize() the storage down to level+1 on every call, so +// the level-0 upload truncated the chain to a single level; the higher level then read back as +// {0,0,0}, IsComplete() rejected the zero-then-nonzero pattern, and DirectGLES answered that by +// skipping the texture's sync entirely. This is the shape KHR-GL33.texture_repeat_mode uses, and +// it accounted for 108 CTS failures in every GL version. +TEST_F(TextureTest, TexImage2DOnLevelZeroKeepsAnAlreadyUploadedHigherLevel) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 49, 23, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 98, 46, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + auto* mipmapObject = static_cast(textureObject.get()); + ASSERT_NE(mipmapObject, nullptr); + EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 2u); + EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0), IntVec3(98, 46, 1)); + EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 1), IntVec3(49, 23, 1)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The other half of the contract: respecifying a level 0 that already held an image still drops +// the chain, exactly as before. Minecraft rebinds the block-atlas name and calls glTexImage2D on +// level 0 before uploading the new levels; leaving the previous chain in place would strand a tail +// at the wrong sizes and - because Mojang terminates its chains with a 0x0 level - reproduce the +// same incomplete-texture black atlas the fix above exists to prevent. +TEST_F(TextureTest, TexImage2DRespecifyingAnExistingLevelZeroDropsTheStaleChain) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + auto* mipmapObject = static_cast(textureObject.get()); + ASSERT_NE(mipmapObject, nullptr); + ASSERT_EQ(mipmapObject->GetMipmapLevelCount(), 3u); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 1u); + EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0), IntVec3(16, 16, 1)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// Same-size respecification has to drop the chain too. The Mipmap Levels video setting rebuilds +// the atlas at identical dimensions with a different level count, so a size-change-only test would +// let the old tail survive. +TEST_F(TextureTest, TexImage2DRespecifyingLevelZeroAtTheSameSizeStillDropsTheChain) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + auto* mipmapObject = static_cast(textureObject.get()); + ASSERT_NE(mipmapObject, nullptr); + EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 1u); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// glTexStorage2D defines exactly `levels` levels. AllocateStorage only grows now, so the immutable +// path has to drop a longer pre-existing chain explicitly. +TEST_F(TextureTest, TexStorage2DTrimsALongerPreExistingMipChain) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 3, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 8, 8); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + auto* mipmapObject = static_cast(textureObject.get()); + ASSERT_NE(mipmapObject, nullptr); + EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 2u); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + TEST_F(TextureTest, TextureStorage3DAndSubImageModifyNamedObjectOnly) { GLuint texture = 0; MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture);