[Feature, Test] (GLImpl): give KHR_debug a real group stack and object labels

This commit is contained in:
2026-08-22 10:42:49 -04:00
parent 6162603072
commit 6bb844b1c1
7 changed files with 599 additions and 23 deletions
+1
View File
@@ -335,6 +335,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
+271
View File
@@ -0,0 +1,271 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.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
#include "GL_Debug.h"
#include <cstring>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
namespace MobileGL::MG_Impl::GLImpl {
namespace {
// Must agree with what GL_Getter answers for GL_MAX_DEBUG_GROUP_STACK_DEPTH and
// GL_MAX_DEBUG_MESSAGE_LENGTH / GL_MAX_LABEL_LENGTH; an application that sizes a buffer
// off the query and then trips a different limit here would have no way to explain it.
constexpr SizeT kMaxDebugGroupStackDepth = 64;
constexpr GLsizei kMaxDebugMessageLength = 1024;
constexpr GLsizei kMaxLabelLength = 256;
// The debug state KHR_debug makes per-context. Held here rather than on GLContext because
// nothing else in MobileGL reads it, and it is keyed on the context id so a
// destroyed-and-recreated context starts with an empty stack and no labels - which the
// unit tests, which recreate the context between cases, depend on.
struct DebugState {
Uint64 contextId = 0;
// The messages pushed with glPushDebugGroup, innermost last. The base group GL creates
// the context with is implicit and is what makes the reported depth start at 1.
Vector<String> groupStack;
// Keyed by (identifier, name); see MakeObjectLabelKey.
UnorderedMap<Uint64, String> objectLabels;
};
DebugState& State() {
static DebugState state;
const Uint64 contextId = MG_State::pGLContext ? MG_State::pGLContext->GetTextureContextId() : 0;
if (state.contextId != contextId) {
state.contextId = contextId;
state.groupStack.clear();
state.objectLabels.clear();
}
return state;
}
Uint64 MakeObjectLabelKey(GLenum identifier, GLuint name) {
return (static_cast<Uint64>(identifier) << 32) | static_cast<Uint64>(name);
}
void RecordDebugError(ErrorCode code, const char* caller, const String& message) {
MG_State::pGLContext->RecordError(code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
}
// GL 4.6 core 20.2: only an APPLICATION or THIRD_PARTY source may be injected; the rest
// are reserved for the implementation itself.
Bool ValidateInjectedSource(GLenum source, const char* caller) {
if (source == GL_DEBUG_SOURCE_APPLICATION || source == GL_DEBUG_SOURCE_THIRD_PARTY) {
return true;
}
RecordDebugError(ErrorCode::InvalidEnum, caller,
std::format("source {} is not GL_DEBUG_SOURCE_APPLICATION or "
"GL_DEBUG_SOURCE_THIRD_PARTY.",
MG_Util::ConvertGLEnumToString(source)));
return false;
}
// A negative length means the string is NUL-terminated (GL 4.6 core 20.2), which is how
// every one of these entry points spells "just use the whole thing".
Bool ValidateDebugStringLength(GLsizei length, const GLchar* text, GLsizei limit, const char* caller,
const char* what) {
const GLsizei effective =
length < 0 ? static_cast<GLsizei>(text != nullptr ? std::strlen(text) : 0) : length;
if (effective < limit) {
return true;
}
RecordDebugError(ErrorCode::InvalidValue, caller,
std::format("{} length {} is not less than the {} limit of {}.", what, effective, what,
limit));
return false;
}
String MakeDebugString(GLsizei length, const GLchar* text) {
if (text == nullptr) return {};
return length < 0 ? String(text) : String(text, static_cast<SizeT>(length));
}
// Whether `name` currently names an object of `identifier`'s type. GL 4.6 core 20.5 makes
// labelling something that does not exist INVALID_VALUE, and every type KHR_debug lists
// has a frontend name check - so this is answered exactly rather than waved through.
// GL_DISPLAY_LIST is deliberately absent: it exists only in the compatibility profile,
// which MobileGL does not expose, so it falls to the INVALID_ENUM path below.
Bool ValidateLabelledObject(GLenum identifier, GLuint name, Bool& outIdentifierKnown) {
outIdentifierKnown = true;
auto* context = MG_State::pGLContext.get();
switch (identifier) {
case GL_BUFFER:
return context->ValidateBufferName(name);
case GL_SHADER:
return context->ValidateShaderName(name);
case GL_PROGRAM:
return context->ValidateProgramName(name);
case GL_VERTEX_ARRAY:
return context->ValidateVertexArrayName(name);
case GL_QUERY:
return IsQuery(name) == GL_TRUE;
case GL_PROGRAM_PIPELINE:
return context->ValidateProgramPipelineName(name);
case GL_TRANSFORM_FEEDBACK:
return context->ValidateTransformFeedbackName(name);
case GL_SAMPLER:
return context->ValidateSamplerName(name);
case GL_TEXTURE:
return context->ValidateTextureName(name);
case GL_RENDERBUFFER:
return context->ValidateRenderbufferName(name);
case GL_FRAMEBUFFER:
// Name 0 is the default framebuffer, which is a real, labellable object.
return name == 0 || context->ValidateFramebufferName(name);
default:
outIdentifierKnown = false;
return false;
}
}
} // namespace
GLint GetDebugGroupStackDepth() {
// GL 4.6 core 20.6: the context is created with one group already on the stack, so the
// reported depth is one more than the number of pushes the application has made.
return static_cast<GLint>(State().groupStack.size()) + 1;
}
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message) {
static_cast<void>(id);
if (!ValidateInjectedSource(source, __func__)) return;
if (!ValidateDebugStringLength(length, message, kMaxDebugMessageLength, __func__, "message")) return;
auto& state = State();
if (state.groupStack.size() + 1 >= kMaxDebugGroupStackDepth) {
// Not INVALID_*: KHR_debug gives the group stack its own error code.
RecordDebugError(ErrorCode::StackOverflow, __func__,
std::format("the debug group stack is already {} deep, which is its maximum.",
kMaxDebugGroupStackDepth));
return;
}
state.groupStack.push_back(MakeDebugString(length, message));
MGLOG_D("glPushDebugGroup(%s) -> depth %d", state.groupStack.back().c_str(), GetDebugGroupStackDepth());
}
void PopDebugGroup() {
auto& state = State();
if (state.groupStack.empty()) {
// The base group the context was created with may not be popped (GL 4.6 core 20.6).
RecordDebugError(ErrorCode::StackUnderflow, __func__,
"the debug group stack holds only the group the context was created with.");
return;
}
MGLOG_D("glPopDebugGroup(%s)", state.groupStack.back().c_str());
state.groupStack.pop_back();
}
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
const GLchar* buf) {
static_cast<void>(id);
if (!ValidateInjectedSource(source, __func__)) return;
switch (type) {
case GL_DEBUG_TYPE_ERROR:
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
case GL_DEBUG_TYPE_PORTABILITY:
case GL_DEBUG_TYPE_PERFORMANCE:
case GL_DEBUG_TYPE_MARKER:
case GL_DEBUG_TYPE_PUSH_GROUP:
case GL_DEBUG_TYPE_POP_GROUP:
case GL_DEBUG_TYPE_OTHER:
break;
default:
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("type {} is not a debug message type.",
MG_Util::ConvertGLEnumToString(type)));
return;
}
switch (severity) {
case GL_DEBUG_SEVERITY_HIGH:
case GL_DEBUG_SEVERITY_MEDIUM:
case GL_DEBUG_SEVERITY_LOW:
case GL_DEBUG_SEVERITY_NOTIFICATION:
break;
default:
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("severity {} is not a debug message severity.",
MG_Util::ConvertGLEnumToString(severity)));
return;
}
if (!ValidateDebugStringLength(length, buf, kMaxDebugMessageLength, __func__, "message")) return;
// No callback is ever invoked and the message log is empty by construction
// (GL_MAX_DEBUG_LOGGED_MESSAGES is 1 and glGetDebugMessageLog returns nothing), so the
// application-visible effect is exactly the error checking above. The text still reaches
// MobileGL's own log, where it is worth having next to the calls it annotates - at debug
// level, so an application that inserts a message per draw costs nothing in a release build.
MGLOG_D("glDebugMessageInsert: %s", MakeDebugString(length, buf).c_str());
}
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
Bool identifierKnown = false;
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
if (!identifierKnown) {
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("identifier {} is not a labellable object type.",
MG_Util::ConvertGLEnumToString(identifier)));
return;
}
if (!objectExists) {
RecordDebugError(ErrorCode::InvalidValue, __func__,
std::format("{} {} is not the name of an existing object.",
MG_Util::ConvertGLEnumToString(identifier), name));
return;
}
if (!ValidateDebugStringLength(length, label, kMaxLabelLength, __func__, "label")) return;
auto& labels = State().objectLabels;
const Uint64 key = MakeObjectLabelKey(identifier, name);
if (label == nullptr) {
// GL 4.6 core 20.5: a NULL label removes any label the object had.
labels.erase(key);
return;
}
labels[key] = MakeDebugString(length, label);
}
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
if (bufSize < 0) {
RecordDebugError(ErrorCode::InvalidValue, __func__, "bufSize must not be negative.");
return;
}
Bool identifierKnown = false;
const Bool objectExists = ValidateLabelledObject(identifier, name, identifierKnown);
if (!identifierKnown) {
RecordDebugError(ErrorCode::InvalidEnum, __func__,
std::format("identifier {} is not a labellable object type.",
MG_Util::ConvertGLEnumToString(identifier)));
return;
}
if (!objectExists) {
RecordDebugError(ErrorCode::InvalidValue, __func__,
std::format("{} {} is not the name of an existing object.",
MG_Util::ConvertGLEnumToString(identifier), name));
return;
}
const auto& labels = State().objectLabels;
const auto it = labels.find(MakeObjectLabelKey(identifier, name));
const String& text = it != labels.end() ? it->second : String{};
// GL 4.6 core 20.5: the returned length excludes the NUL, and an unlabelled object hands
// back an empty string with length 0 rather than an error.
SizeT copied = 0;
if (label != nullptr && bufSize > 0) {
copied = std::min(text.size(), static_cast<SizeT>(bufSize) - 1);
std::memcpy(label, text.data(), copied);
label[copied] = '\0';
}
if (length != nullptr) {
*length = static_cast<GLsizei>(copied);
}
}
} // namespace MobileGL::MG_Impl::GLImpl
+42
View File
@@ -0,0 +1,42 @@
// MobileGL - MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.h
// 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
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Impl::GLImpl {
// KHR_debug, core since GL 4.3 (GL 4.6 core 20). Applications use these to annotate a capture
// and to name their objects; Better Clouds calls all four for exactly that.
//
// MobileGL implements the STATE and the ERRORS, and deliberately does not forward the calls to
// the host driver. Two independent reasons:
//
// * glObjectLabel names a FRONTEND object. MobileGL's texture 5 is not the ES driver's
// texture 5 (and under DirectVulkan it is not a driver object at all), so forwarding the
// pair verbatim would label an unrelated object or a nonexistent one - worse than not
// labelling.
// * A debug GROUP is only meaningful if it brackets the commands the application issued
// inside it. Neither backend emits its work at the moment the GL call arrives: DirectGLES
// defers and reorders state sync and uploads around draws, and DirectVulkan is usually not
// even recording a command buffer here. A forwarded push/pop would therefore enclose the
// wrong commands, which is a misleading capture rather than a helpful one.
//
// What the application can rely on is the observable contract: the group stack depth is real
// (GL_DEBUG_GROUP_STACK_DEPTH tracks it, and over/underflow raise the errors KHR_debug
// specifies), and a label written with glObjectLabel comes back from glGetObjectLabel.
void PushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar* message);
void PopDebugGroup();
void DebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
const GLchar* buf);
void ObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label);
void GetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label);
// Current depth of the debug group stack, for GL_DEBUG_GROUP_STACK_DEPTH. The base group the
// context is created with counts, so this is never below 1 (GL 4.6 core 20.6).
GLint GetDebugGroupStackDepth();
} // namespace MobileGL::MG_Impl::GLImpl
@@ -20,6 +20,7 @@
#include "../Framebuffer/GL_Framebuffer.h"
#include "../VertexArray/GL_VertexArray.h"
#include "../Sync/GL_Sync.h"
#include "../Debug/GL_Debug.h"
#include <MG_State/GLState/Core.h>
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
@@ -378,27 +379,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexBindingDivisor, GLuint bindingindex, GLuint
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrier) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrier)
DECLARE_GL_FUNCTION_HEAD(void, CopyImageSubData, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyImageSubData, srcName, srcTarget, srcLevel, srcX, srcY, srcZ, dstName, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageControl, GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageControl, source, type, severity, count, ids, enabled)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
DECLARE_GL_FUNCTION_HEAD(void, DebugMessageInsert, GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DebugMessageInsert, source, type, id, severity, length, buf)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DebugMessageCallback, GLDEBUGPROC callback, const void* userParam) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DebugMessageCallback, callback, userParam)
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetDebugMessageLog, GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetDebugMessageLog, count, bufSize, sources, types, ids, severities, lengths, messageLog)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopDebugGroup)
MOBILEGL_GL_API void glObjectLabel(GLenum identifier, GLuint name, GLsizei length, const GLchar* label) {
(void)identifier;
(void)name;
(void)length;
(void)label;
}
MOBILEGL_GL_API void glGetObjectLabel(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) {
(void)identifier;
(void)name;
if (length) {
*length = 0;
}
if (label && bufSize > 0) {
label[0] = '\0';
}
}
DECLARE_GL_FUNCTION_HEAD(void, PushDebugGroup, GLenum source, GLuint id, GLsizei length, const GLchar* message) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PushDebugGroup, source, id, length, message)
DECLARE_GL_FUNCTION_HEAD(void, PopDebugGroup) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PopDebugGroup)
DECLARE_GL_FUNCTION_HEAD(void, ObjectLabel, GLenum identifier, GLuint name, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ObjectLabel, identifier, name, length, label)
DECLARE_GL_FUNCTION_HEAD(void, GetObjectLabel, GLenum identifier, GLuint name, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetObjectLabel, identifier, name, bufSize, length, label)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ObjectPtrLabel, const void* ptr, GLsizei length, const GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ObjectPtrLabel, ptr, length, label)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectPtrLabel, const void* ptr, GLsizei bufSize, GLsizei* length, GLchar* label) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectPtrLabel, ptr, bufSize, length, label)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetPointerv, GLenum pname, void** params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetPointerv, pname, params)
+7 -4
View File
@@ -10,6 +10,7 @@
#include <cmath>
#include <Config.h>
#include <MGGitHash.h>
#include <MG_Impl/GLImpl/Debug/GL_Debug.h>
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/EGLState/Core.h>
#include <MG_State/GLState/Core.h>
@@ -1427,19 +1428,21 @@ namespace MobileGL::MG_Impl::GLImpl {
: 0;
return;
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
// KHR_debug floors this at 64 even when the group entry points are stubs: the
// limit describes how deep glPushDebugGroup may nest, and 0 is not a legal answer.
// KHR_debug floors this at 64. It must agree with what GL_Debug.cpp actually enforces,
// or an application that nests to the reported limit would take a STACK_OVERFLOW.
*params = kFrontendMaxDebugGroupStackDepth;
return;
case GL_MAX_DEBUG_MESSAGE_LENGTH:
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
*params = 1024; // agrees with GL_Debug.cpp's kMaxDebugMessageLength
return;
case GL_MAX_DEBUG_LOGGED_MESSAGES:
// Size of the message log ring; KHR_debug requires at least 1.
*params = kFrontendMaxDebugLoggedMessages;
return;
case GL_DEBUG_GROUP_STACK_DEPTH:
*params = 0; // debug-group entrypoints are stubbed
// The live depth, which is never 0: GL 4.6 core 20.6 creates the context with one
// group already on the stack, and that is the one glPopDebugGroup may not pop.
*params = GetDebugGroupStackDepth();
return;
case GL_CONTEXT_FLAGS: {
*params = MG_State::pEGLContext ? MG_State::pEGLContext->GetCurrentContextFlags() : 0;
+19
View File
@@ -1,5 +1,24 @@
cmake_minimum_required(VERSION 3.14)
add_executable(
DebugTest
DebugTest.cpp
)
target_include_directories(DebugTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
DebugTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(DebugTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
ObjectLifetimeIdTest
ObjectLifetimeIdTest.cpp
+253
View File
@@ -0,0 +1,253 @@
// MobileGL - MobileGL/MG_Test/State/DebugTest.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
//
// KHR_debug (GL 4.6 core 20), the part MobileGL actually implements: the debug group stack and
// object labels. These were silent stubs - glPushDebugGroup logged once and returned, glObjectLabel
// discarded its argument and glGetObjectLabel always answered with an empty string - which meant
// GL_DEBUG_GROUP_STACK_DEPTH reported 0 (not a legal value; the context is created with one group
// already on the stack) and a label never survived being written.
//
// The calls are deliberately NOT forwarded to the host driver; GL_Debug.h explains why. What the
// tests below pin is the observable contract that remains: the stack depth is real and its
// over/underflow errors are the ones KHR_debug names, and a label written comes back.
#include <gtest/gtest.h>
#include <string>
#include "Includes.h"
#include "Init.h"
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Debug/GL_Debug.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_State/GLState/Core.h>
using namespace MobileGL;
namespace {
class DebugTest : public ::testing::Test {
protected:
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();
// The group stack is context state and this binary shares one context across cases,
// so unwind whatever a previous case left pushed.
while (StackDepth() > 1) {
MG_Impl::GLImpl::PopDebugGroup();
}
DrainPendingGlErrors();
}
void TearDown() override {
while (StackDepth() > 1) {
MG_Impl::GLImpl::PopDebugGroup();
}
DrainPendingGlErrors();
}
static GLint StackDepth() {
GLint depth = -1;
MG_Impl::GLImpl::GetIntegerv(GL_DEBUG_GROUP_STACK_DEPTH, &depth);
return depth;
}
static GLuint GenTexture() {
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
return texture;
}
};
TEST_F(DebugTest, StackDepthStartsAtOneAndTracksPushesAndPops) {
// GL 4.6 core 20.6: the context is created with one group on the stack, so 0 is never a
// legal answer - which is what the old stub reported.
EXPECT_EQ(StackDepth(), 1);
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 1, -1, "outer");
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(StackDepth(), 2);
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_THIRD_PARTY, 2, -1, "inner");
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(StackDepth(), 3);
MG_Impl::GLImpl::PopDebugGroup();
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(StackDepth(), 2);
MG_Impl::GLImpl::PopDebugGroup();
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(StackDepth(), 1);
}
TEST_F(DebugTest, PoppingTheBaseGroupIsStackUnderflow) {
ASSERT_EQ(StackDepth(), 1);
MG_Impl::GLImpl::PopDebugGroup();
ExpectSingleGlError(GL_STACK_UNDERFLOW);
EXPECT_EQ(StackDepth(), 1) << "a refused pop must not move the stack";
}
TEST_F(DebugTest, PushingPastTheAdvertisedLimitIsStackOverflow) {
GLint limit = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEBUG_GROUP_STACK_DEPTH, &limit);
ASSERT_GE(limit, 64) << "KHR_debug floors GL_MAX_DEBUG_GROUP_STACK_DEPTH at 64";
// Nesting exactly to the advertised limit must WORK - an implementation whose real limit
// is lower than the one it reports is worse than one that reports a lower limit.
for (GLint i = 1; i < limit; ++i) {
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 0, -1, "deep");
}
DrainPendingGlErrors();
EXPECT_EQ(StackDepth(), limit);
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 0, -1, "too deep");
ExpectSingleGlError(GL_STACK_OVERFLOW);
EXPECT_EQ(StackDepth(), limit) << "a refused push must not move the stack";
}
TEST_F(DebugTest, OnlyApplicationAndThirdPartySourcesMayBePushed) {
// 20.2 reserves every other source for the implementation.
MG_Impl::GLImpl::PushDebugGroup(GL_DEBUG_SOURCE_API, 0, -1, "not mine to push");
ExpectSingleGlError(GL_INVALID_ENUM);
EXPECT_EQ(StackDepth(), 1);
}
TEST_F(DebugTest, DebugMessageInsertValidatesItsEnums) {
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_MARKER, 0,
GL_DEBUG_SEVERITY_NOTIFICATION, -1, "hello");
ExpectSingleGlError(GL_NO_ERROR);
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_MARKER, 0,
GL_DEBUG_SEVERITY_NOTIFICATION, -1, "bad source");
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_TEXTURE_2D, 0,
GL_DEBUG_SEVERITY_NOTIFICATION, -1, "bad type");
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_MARKER, 0, GL_TEXTURE_2D, -1,
"bad severity");
ExpectSingleGlError(GL_INVALID_ENUM);
}
TEST_F(DebugTest, AMessageLongerThanTheAdvertisedLimitIsInvalidValue) {
GLint limit = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEBUG_MESSAGE_LENGTH, &limit);
ASSERT_GT(limit, 0);
const std::string tooLong(static_cast<std::size_t>(limit) + 1, 'x');
MG_Impl::GLImpl::DebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_MARKER, 0,
GL_DEBUG_SEVERITY_NOTIFICATION, -1, tooLong.c_str());
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(DebugTest, ALabelWrittenComesBack) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
DrainPendingGlErrors();
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, -1, "coverage_stencil");
ExpectSingleGlError(GL_NO_ERROR);
GLchar buffer[64] = {};
GLsizei length = -1;
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(buffer), &length, buffer);
ExpectSingleGlError(GL_NO_ERROR);
// 20.5: the returned length excludes the terminator.
EXPECT_EQ(length, static_cast<GLsizei>(std::string("coverage_stencil").size()));
EXPECT_STREQ(buffer, "coverage_stencil");
}
TEST_F(DebugTest, LabelsAreScopedToTheObjectAndItsType) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
GLuint buffer = 0;
MG_Impl::GLImpl::GenBuffers(1, &buffer);
MG_Impl::GLImpl::BindBuffer(GL_ARRAY_BUFFER, buffer);
DrainPendingGlErrors();
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, -1, "the texture");
MG_Impl::GLImpl::ObjectLabel(GL_BUFFER, buffer, -1, "the buffer");
DrainPendingGlErrors();
GLchar textureLabel[32] = {};
GLchar bufferLabel[32] = {};
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(textureLabel), nullptr, textureLabel);
MG_Impl::GLImpl::GetObjectLabel(GL_BUFFER, buffer, sizeof(bufferLabel), nullptr, bufferLabel);
DrainPendingGlErrors();
// The two names may collide numerically - they are separate namespaces - so a label store
// keyed on the name alone would hand one object's label to the other.
EXPECT_STREQ(textureLabel, "the texture");
EXPECT_STREQ(bufferLabel, "the buffer");
}
TEST_F(DebugTest, AnUnlabelledObjectAnswersWithAnEmptyString) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
DrainPendingGlErrors();
GLchar buffer[8] = {'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x'};
GLsizei length = -1;
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(buffer), &length, buffer);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(length, 0);
EXPECT_STREQ(buffer, "");
}
TEST_F(DebugTest, ALabelIsTruncatedToTheBufferAndStaysTerminated) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, -1, "abcdefgh");
DrainPendingGlErrors();
GLchar buffer[4] = {};
GLsizei length = -1;
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(buffer), &length, buffer);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(length, 3) << "bufSize includes the terminator, so only bufSize-1 characters fit";
EXPECT_STREQ(buffer, "abc");
}
TEST_F(DebugTest, LabellingSomethingThatDoesNotExistIsInvalidValue) {
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, 0xFFFFFFFFu, -1, "nothing");
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(DebugTest, LabellingANonObjectTypeIsInvalidEnum) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
DrainPendingGlErrors();
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE_2D, texture, -1, "not an object type");
ExpectSingleGlError(GL_INVALID_ENUM);
}
TEST_F(DebugTest, ANullLabelRemovesTheLabel) {
const GLuint texture = GenTexture();
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, -1, "temporary");
MG_Impl::GLImpl::ObjectLabel(GL_TEXTURE, texture, 0, nullptr);
DrainPendingGlErrors();
GLsizei length = -1;
GLchar buffer[16] = {};
MG_Impl::GLImpl::GetObjectLabel(GL_TEXTURE, texture, sizeof(buffer), &length, buffer);
ExpectSingleGlError(GL_NO_ERROR);
EXPECT_EQ(length, 0);
}
} // namespace