[Fix, Test] (MG_Impl, MG_State): GL entry points record errors instead of throwing through the C ABI - CopyTexImage superset rule, TEXTURE_BUFFER level queries, indexed cap toggles

This commit is contained in:
2026-08-11 02:24:41 -04:00
parent efa0345c36
commit 5c8a9c41d6
8 changed files with 363 additions and 21 deletions
+8 -1
View File
@@ -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<int>(stateTextureObject->GetStorageType()),
stateTextureObject->GetExternalIndex());
break;
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
+40 -9
View File
@@ -613,6 +613,23 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("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<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
"Level queries are not supported for texture-buffer storage."));
}
} // namespace
const SharedPtr<MG_State::GLState::ITextureObject>& 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<GenericErrorInfo>("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,
+69 -7
View File
@@ -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<GenericErrorInfo>(
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<GenericErrorInfo>(
"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
@@ -40,5 +40,9 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
TextureTarget target);
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& 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
@@ -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<int>(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<int>(cap), index);
return false;
}
if (index >= MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS) {
+25
View File
@@ -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)
@@ -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 <gtest/gtest.h>
#include "Includes.h"
#include "Init.h"
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
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);
}
+112
View File
@@ -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);
}
}