diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index ac4335e5..c1bfc92a 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2809,7 +2809,14 @@ namespace MobileGL::MG_Backend::DirectGLES { break; } default: - THROW_UNIMPL_EXCEPTION; + // TextureStorageType is {Mipmap, Buffer}, both handled above, so this is a + // backstop for a state object that grew a new storage kind. Skipping the upload + // renders wrong; throwing unwinds through the C GL ABI and kills the process. + MGLOG_I("DirectGLES texture sync: no upload path for storage type %d on texture %u; " + "skipping this sync", + static_cast(stateTextureObject->GetStorageType()), + stateTextureObject->GetExternalIndex()); + break; } DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index b1eae2ef..e365b558 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -613,6 +613,23 @@ namespace MobileGL::MG_Impl::GLImpl { MakeUnique("MG_Impl/GLImpl", caller, "Compressed texture formats are not supported.")); } + + // glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only + // other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is + // {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead + // of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes + // the process down, which is never an acceptable answer to a query - see the same reasoning + // above for the compressed-format path. + void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) { + MGLOG_I("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer " + "storage; recording GL_INVALID_OPERATION instead of terminating", + caller, MG_Util::ConvertGLEnumToString(pname).c_str()); + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", caller, + "Level queries are not supported for texture-buffer storage.")); + } } // namespace const SharedPtr& GetTextureObjectByName(GLuint texture, const char* caller) { @@ -2910,7 +2927,8 @@ namespace MobileGL::MG_Impl::GLImpl { break; } default: - THROW_UNIMPL_EXCEPTION; + RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); + break; } } break; @@ -2924,7 +2942,8 @@ namespace MobileGL::MG_Impl::GLImpl { break; } default: - THROW_UNIMPL_EXCEPTION; + RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); + break; } } break; @@ -2938,7 +2957,8 @@ namespace MobileGL::MG_Impl::GLImpl { break; } default: - THROW_UNIMPL_EXCEPTION; + RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname); + break; } } break; @@ -3045,7 +3065,8 @@ namespace MobileGL::MG_Impl::GLImpl { break; } default: - THROW_UNIMPL_EXCEPTION; + RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); + break; } } break; @@ -3059,7 +3080,8 @@ namespace MobileGL::MG_Impl::GLImpl { break; } default: - THROW_UNIMPL_EXCEPTION; + RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); + break; } } break; @@ -3073,7 +3095,8 @@ namespace MobileGL::MG_Impl::GLImpl { break; } default: - THROW_UNIMPL_EXCEPTION; + RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname); + break; } } break; @@ -3403,7 +3426,10 @@ namespace MobileGL::MG_Impl::GLImpl { GET_SRC_INTERNAL_FORMAT(readBufferType); } - if (!TextureImpl::ValidateBaseInternalFormatMatch(internalFormat, srcInternalFormat)) THROW_UNIMPL_EXCEPTION; + // The validator has already recorded GL_INVALID_OPERATION; just decline. Throwing + // here unwound a C++ exception through the C GL ABI and killed the process (see the + // same reasoning at :604-609). + if (!TextureImpl::ValidateCopyTexImageBaseFormatSubset(internalFormat, srcInternalFormat)) return false; GLenum outInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(srcInternalFormat); GLenum realInternalFormat = GL_RGBA8; @@ -3426,8 +3452,13 @@ namespace MobileGL::MG_Impl::GLImpl { void CopyTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border) { - // TODO: implement - THROW_UNIMPL_EXCEPTION; + // 1D textures are not implemented by this backend set. Record the error the way every + // other unsupported entry point does - throwing unwinds through the C GL ABI and kills + // the process, which is never an acceptable answer to an unsupported call. + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", "CopyTexImage1D", + "1D textures are not supported by this implementation")); } void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index 07ed2d2f..3e75cf10 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -424,19 +424,81 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } + namespace { + // Component set of an UNSIZED base internal format, as the bitmask GL 4.6 SS 8.6 + // reasons about. Colour components are independent bits so "subset" is a plain + // mask test; depth and stencil are their own components and never satisfy a + // colour request (or each other). + enum : Uint32 { + kComponentR = 1u << 0, + kComponentG = 1u << 1, + kComponentB = 1u << 2, + kComponentA = 1u << 3, + kComponentDepth = 1u << 4, + kComponentStencil = 1u << 5, + }; + + Uint32 BaseFormatComponents(TextureInternalFormat unsizedFormat) { + switch (unsizedFormat) { + case TextureInternalFormat::Red: + return kComponentR; + case TextureInternalFormat::RG: + return kComponentR | kComponentG; + case TextureInternalFormat::RGB: + return kComponentR | kComponentG | kComponentB; + case TextureInternalFormat::RGBA: + return kComponentR | kComponentG | kComponentB | kComponentA; + case TextureInternalFormat::DepthComponent: + return kComponentDepth; + case TextureInternalFormat::DepthStencil: + return kComponentDepth | kComponentStencil; + default: + return 0; + } + } + } // namespace + Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2) { - auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1); - auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2); + const auto unsizedFormat1 = MG_Util::ConvertInternalFormatToUnsized(format1); + const auto unsizedFormat2 = MG_Util::ConvertInternalFormatToUnsized(format2); if (unsizedFormat1 != unsizedFormat2) { + // The 3-argument GenericErrorInfo constructor used to be spelled as a single + // std::format() call whose format string was the component name, so every + // diagnostic collapsed to the literal "MG_Impl/GLImpl". Format the message, then + // hand over component/function/message separately. MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( - std::format("MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch", - "The base internal format of the two formats do not match ({} vs. {})", - MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1).c_str(), - MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2).c_str()))); + "MG_Impl/GLImpl", "ValidateBaseInternalFormatMatch", + std::format("The base internal format of the two formats do not match ({} vs. {})", + MG_Util::ConvertTextureInternalFormatToString(unsizedFormat1), + MG_Util::ConvertTextureInternalFormatToString(unsizedFormat2)))); return false; } return true; - } // namespace TextureImpl + } + + Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat) { + const auto unsizedDest = MG_Util::ConvertInternalFormatToUnsized(destFormat); + const auto unsizedSrc = MG_Util::ConvertInternalFormatToUnsized(srcFormat); + // GL 4.6 SS 8.6: glCopyTexImage* may request a SUBSET of the read buffer's components, + // not an exact match - GL_RGB from an RGBA8 framebuffer is textbook legal and is what + // Minecraft and its mods do. glCopyTexImage2D used to run the exact-match predicate + // above and turn its rejection into an uncaught exception through the C GL ABI, so the + // app died rather than seeing a GL error. + const Uint32 destComponents = BaseFormatComponents(unsizedDest); + const Uint32 srcComponents = BaseFormatComponents(unsizedSrc); + if (destComponents == 0 || srcComponents == 0 || (destComponents & ~srcComponents) != 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique( + "MG_Impl/GLImpl", "ValidateCopyTexImageBaseFormatSubset", + std::format("the read buffer's base internal format {} does not provide every component of " + "the requested internal format {}", + MG_Util::ConvertTextureInternalFormatToString(unsizedSrc), + MG_Util::ConvertTextureInternalFormatToString(unsizedDest)))); + return false; + } + return true; + } } // namespace MobileGL::MG_Impl::GLImpl::TextureImpl diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index b5d40b8c..7ebd14ff 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -40,5 +40,9 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { TextureTarget target); Bool ValidateTextureSubImageOffsets(const SharedPtr& textureObject, Int xoffset, Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0); + // Exact base-format equality - what glCopyImageSubData's format compatibility needs. Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2); + // GL 4.6 SS 8.6 subset rule for glCopyTexImage*: the read buffer must supply every component + // the requested internalformat asks for, but may supply more. + Bool ValidateCopyTexImageBaseFormatSubset(TextureInternalFormat destFormat, TextureInternalFormat srcFormat); } // namespace MobileGL::MG_Impl::GLImpl::TextureImpl diff --git a/MobileGL/MG_State/GLState/RenderState/RenderState.cpp b/MobileGL/MG_State/GLState/RenderState/RenderState.cpp index 06a07ecd..c0feabf8 100644 --- a/MobileGL/MG_State/GLState/RenderState/RenderState.cpp +++ b/MobileGL/MG_State/GLState/RenderState/RenderState.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "RenderState.h" +#include "MG_Util/Debug/Log.h" #include "MG_Util/Types.h" namespace MobileGL { @@ -268,9 +269,14 @@ namespace MobileGL { } void RenderState::SetCapabilityIndexed(CapabilityInput cap, Uint index, Bool enabled) { - // Only for BlendState currently + // Only for BlendState currently. The GL entry points (glEnablei/glDisablei) already + // reject every non-GL_BLEND target with GL_INVALID_ENUM before reaching here, so this + // is a backstop - but it must stay a backstop: THROW_UNIMPL_EXCEPTION unwinds a C++ + // exception through the C GL ABI and terminates the process. if (cap != CapabilityInput::Blend) { - THROW_UNIMPL_EXCEPTION; + MGLOG_I("RenderState::SetCapabilityIndexed: indexed capability state exists only for " + "GL_BLEND (cap=%d, index=%u); ignoring", + static_cast(cap), index); return; } if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) { @@ -284,9 +290,13 @@ namespace MobileGL { } Bool RenderState::IsCapabilityEnabledIndexed(CapabilityInput cap, Uint index) const { - // Only for BlendState currently + // Only for BlendState currently - same backstop reasoning as SetCapabilityIndexed: + // glIsEnabledi has already answered GL_INVALID_ENUM/GL_FALSE for anything else, and a + // query must never be able to terminate the process. if (cap != CapabilityInput::Blend) { - THROW_UNIMPL_EXCEPTION; + MGLOG_I("RenderState::IsCapabilityEnabledIndexed: indexed capability state exists only " + "for GL_BLEND (cap=%d, index=%u); reporting disabled", + static_cast(cap), index); return false; } if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) { diff --git a/MobileGL/MG_Test/State/CMakeLists.txt b/MobileGL/MG_Test/State/CMakeLists.txt index bb8e5900..63e3f664 100644 --- a/MobileGL/MG_Test/State/CMakeLists.txt +++ b/MobileGL/MG_Test/State/CMakeLists.txt @@ -25,3 +25,28 @@ endif() include(GoogleTest) gtest_discover_tests(ObjectLifetimeIdTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) + +add_executable( + RenderStateTest + RenderStateTest.cpp +) + +target_include_directories(RenderStateTest PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL + ${MGL_ROOT}/3rdparty/xxHash + ${MGL_ROOT}/3rdparty/Vulkan-Headers/include + ${MGL_ROOT}/3rdparty/SPIRV-Reflect +) + +target_link_libraries( + RenderStateTest PRIVATE + GTest::gtest_main + ${LINK_LIBRARIES} +) + +if (MSVC) + target_compile_options(RenderStateTest PRIVATE /Zc:preprocessor) +endif() + +gtest_discover_tests(RenderStateTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) diff --git a/MobileGL/MG_Test/State/RenderStateTest.cpp b/MobileGL/MG_Test/State/RenderStateTest.cpp new file mode 100644 index 00000000..8a5ee533 --- /dev/null +++ b/MobileGL/MG_Test/State/RenderStateTest.cpp @@ -0,0 +1,91 @@ +// MobileGL - MobileGL/MG_Test/State/RenderStateTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Indexed capability state (glEnablei/glDisablei/glIsEnabledi) exists only for GL_BLEND in this +// stack. Every other capability must come back as GL_INVALID_ENUM per GL 4.6 sec. 17.3.3 - and, +// far more importantly, must come back at all: RenderState::SetCapabilityIndexed and +// IsCapabilityEnabledIndexed used to answer a non-blend capability with THROW_UNIMPL_EXCEPTION, +// which unwinds a C++ exception through the C GL ABI and terminates the process. + +#include + +#include "Includes.h" +#include "Init.h" + +#include +#include +#include +#include + +using namespace MobileGL; + +namespace { + class RenderStateTest: public ::testing::Test { + protected: + // GL error flags are sticky per code and the context outlives an individual test in this + // binary, so a pending error from an earlier case would be handed to the next GetError(). + static void DrainPendingGlErrors() { + for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) { + } + } + + static void ExpectSingleGlError(GLenum expected) { + EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + } + + void SetUp() override { + MobileGL::Initialize(); + DrainPendingGlErrors(); + } + + void TearDown() override { + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind"; + } + }; +} // namespace + +TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectNonBlendCapabilities) { + // GL_CLIP_DISTANCE0 is a real capability, just not an indexed one - the shape an application or + // a CTS negative test would hit. + for (const GLenum cap : {GL_CLIP_DISTANCE0, GL_DEPTH_TEST, GL_SCISSOR_TEST}) { + MG_Impl::GLImpl::Enablei(cap, 0); + ExpectSingleGlError(GL_INVALID_ENUM); + + MG_Impl::GLImpl::Disablei(cap, 0); + ExpectSingleGlError(GL_INVALID_ENUM); + + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(cap, 0), GL_FALSE); + ExpectSingleGlError(GL_INVALID_ENUM); + } +} + +TEST_F(RenderStateTest, IndexedCapabilityTogglesRejectAnOutOfRangeBufferIndex) { + const GLuint outOfRange = MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; + + MG_Impl::GLImpl::Enablei(GL_BLEND, outOfRange); + ExpectSingleGlError(GL_INVALID_VALUE); + + MG_Impl::GLImpl::Disablei(GL_BLEND, outOfRange); + ExpectSingleGlError(GL_INVALID_VALUE); + + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, outOfRange), GL_FALSE); + ExpectSingleGlError(GL_INVALID_VALUE); +} + +TEST_F(RenderStateTest, IndexedBlendTogglesStillWork) { + // The rejection path must not have cost the one capability that is genuinely indexed. + MG_Impl::GLImpl::Enablei(GL_BLEND, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_TRUE); + + MG_Impl::GLImpl::Disablei(GL_BLEND, 1); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(MG_Impl::GLImpl::IsEnabledi(GL_BLEND, 1), GL_FALSE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 70ef7da7..e215c11e 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -3176,3 +3176,115 @@ TEST_F(TextureTest, WidenedRenderTargetUploadExpandsThreeChannelDataWithOpaqueAl EXPECT_EQ(PrepareChannelWidenedUpload(3, texelSize, nullptr, 0, GL_FLOAT, widened), nullptr); } } + +// --------------------------------------------------------------------------------------------- +// A GL entry point may return an error, but it may never throw through the C GL ABI: unwinding a +// C++ exception across it terminates the process. These cover the sites that used to do exactly +// that (KHR-GL30.api.coverage died on the first of them on both backends). +// --------------------------------------------------------------------------------------------- + +namespace { + struct CopyTexImage2DCall { + Bool Called = false; + GLenum Target = 0; + GLint Level = 0; + GLenum InternalFormat = 0; + GLsizei Width = 0; + GLsizei Height = 0; + }; + + CopyTexImage2DCall g_copyTexImage2DCall; + + void RecordCopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint, GLint, GLsizei width, + GLsizei height, GLint) { + g_copyTexImage2DCall = {true, target, level, internalformat, width, height}; + } + + // A colour read framebuffer of the requested sized format, bound to GL_READ_FRAMEBUFFER, which + // is what glCopyTexImage2D takes its source base format from. + void BindReadFramebufferWithColorFormat(GLenum sizedInternalFormat) { + GLuint framebuffer = 0; + GLuint texture = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); + MG_Impl::GLImpl::TextureStorage2D(texture, 1, sizedInternalFormat, 16, 16); + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0); + MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + } + + GLuint BindFreshMutableTexture2D() { + GLuint texture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + return texture; + } +} // namespace + +TEST_F(TextureTest, CopyTexImage2DAcceptsEveryComponentSubsetOfTheReadBuffer) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D = RecordCopyTexImage2D; + + BindReadFramebufferWithColorFormat(GL_RGBA8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "read framebuffer setup itself failed"; + + // GL 4.6 sec. 8.6: internalformat may name a SUBSET of the read buffer's components. This is + // exactly the list KHR-GL30.api.coverage walks against an rgba8888 colour buffer, and it is + // also what an ordinary GL app does with glCopyTexImage2D(GL_RGB) from an RGBA8 framebuffer. + for (const GLenum internalFormat : {GL_RED, GL_RG, GL_RGB, GL_RGBA}) { + BindFreshMutableTexture2D(); + g_copyTexImage2DCall = {}; + + MG_Impl::GLImpl::CopyTexImage2D(GL_TEXTURE_2D, 0, internalFormat, 0, 0, 1, 1, 0); + + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "internalformat " << internalFormat; + EXPECT_TRUE(g_copyTexImage2DCall.Called) << "internalformat " << internalFormat; + EXPECT_EQ(g_copyTexImage2DCall.InternalFormat, internalFormat); + EXPECT_EQ(g_copyTexImage2DCall.Width, 1); + EXPECT_EQ(g_copyTexImage2DCall.Height, 1); + } +} + +TEST_F(TextureTest, CopyTexImage2DRejectsAFormatTheReadBufferCannotSupply) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D = RecordCopyTexImage2D; + + BindReadFramebufferWithColorFormat(GL_R8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "read framebuffer setup itself failed"; + + BindFreshMutableTexture2D(); + g_copyTexImage2DCall = {}; + + // The subset rule still has a wrong side: GL_RGBA asks for components a GL_R8 read buffer does + // not have. That must be GL_INVALID_OPERATION and nothing else - not a throw, not silence. + MG_Impl::GLImpl::CopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 1, 1, 0); + + ExpectSingleGlError(GL_INVALID_OPERATION); + EXPECT_FALSE(g_copyTexImage2DCall.Called) << "a rejected copy must not reach the backend"; +} + +TEST_F(TextureTest, CopyTexImage1DReportsUnsupportedInsteadOfTerminating) { + // 1D textures have no upload path in this stack; the entry point used to throw unconditionally. + MG_Impl::GLImpl::CopyTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 0, 0, 1, 0); + ExpectSingleGlError(GL_INVALID_OPERATION); +} + +TEST_F(TextureTest, GetTexLevelParameterOnBufferStorageReportsErrorInsteadOfTerminating) { + // TextureStorageType is {Mipmap, Buffer} and the level queries only answer out of a mipmap + // chain, so every glGetTexLevelParameter* on a GL_TEXTURE_BUFFER texture reached a + // THROW_UNIMPL_EXCEPTION default: label and killed the process. + GLuint texture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_BUFFER, 1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, texture); + MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0); + DrainPendingGlErrors(); + + for (const GLenum pname : {GL_TEXTURE_WIDTH, GL_TEXTURE_HEIGHT, GL_TEXTURE_DEPTH}) { + GLint intParam = 0x20202020; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &intParam); + ExpectSingleGlError(GL_INVALID_OPERATION); + + GLfloat floatParam = 12345.0f; + MG_Impl::GLImpl::GetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &floatParam); + ExpectSingleGlError(GL_INVALID_OPERATION); + } +}