diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e573f41a..b25e06b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,6 +74,9 @@ jobs: node-version: '24' cache: 'pnpm' + - name: Check GL boundary (no gl* outside GLDevice) + run: node tools/check-gl-boundary.mjs + - name: Install dependencies run: pnpm install diff --git a/src/esengine/bindings/RendererBindings.cpp b/src/esengine/bindings/RendererBindings.cpp index 371b825b..5f354828 100644 --- a/src/esengine/bindings/RendererBindings.cpp +++ b/src/esengine/bindings/RendererBindings.cpp @@ -2,7 +2,6 @@ #include "RendererBindings.hpp" #include "ActiveContext.hpp" -#include "../renderer/OpenGLHeaders.hpp" #include "../renderer/GfxDevice.hpp" #include "../renderer/RenderFrame.hpp" #include "../renderer/RenderContext.hpp" @@ -52,19 +51,9 @@ static EstellaContext& ctx() { return activeCtx(); } static u32 checkGLErrors(const char* context) { if (!g_glErrorCheckEnabled) return 0; u32 errorCount = 0; - GLenum err; - while ((err = static_cast(g_device->getError())) != GL_NO_ERROR) { - const char* errStr = "UNKNOWN"; - switch (err) { - case GL_INVALID_ENUM: errStr = "INVALID_ENUM"; break; - case GL_INVALID_VALUE: errStr = "INVALID_VALUE"; break; - case GL_INVALID_OPERATION: errStr = "INVALID_OPERATION"; break; - case GL_INVALID_FRAMEBUFFER_OPERATION: errStr = "INVALID_FRAMEBUFFER_OPERATION"; break; - case GL_OUT_OF_MEMORY: errStr = "OUT_OF_MEMORY"; break; - case GL_CONTEXT_LOST: errStr = "CONTEXT_LOST"; break; - case GL_CONTEXT_LOST_WEBGL: errStr = "CONTEXT_LOST_WEBGL"; break; - } - ES_LOG_ERROR("[GL Error] {} (0x{:04X}) at: {}", errStr, static_cast(err), context); + u32 err; + while ((err = g_device->getError()) != 0) { + ES_LOG_ERROR("[GL Error] 0x{:04X} at: {}", err, context); errorCount++; } return errorCount; @@ -610,29 +599,15 @@ void renderer_diagnose() { return; } - const char* version = reinterpret_cast(glGetString(GL_VERSION)); - const char* rendererStr = reinterpret_cast(glGetString(GL_RENDERER)); - const char* vendor = reinterpret_cast(glGetString(GL_VENDOR)); - const char* slVersion = reinterpret_cast(glGetString(GL_SHADING_LANGUAGE_VERSION)); - ES_LOG_INFO("[Diagnose] GL Version: {}", version ? version : "null"); - ES_LOG_INFO("[Diagnose] GL Renderer: {}", rendererStr ? rendererStr : "null"); - ES_LOG_INFO("[Diagnose] GL Vendor: {}", vendor ? vendor : "null"); - ES_LOG_INFO("[Diagnose] GLSL Version: {}", slVersion ? slVersion : "null"); - - GLint viewport[4]; - glGetIntegerv(GL_VIEWPORT, viewport); - ES_LOG_INFO("[Diagnose] GL Viewport: {}x{} at ({},{})", viewport[2], viewport[3], viewport[0], viewport[1]); + ES_LOG_INFO("[Diagnose] GL Version: {}", g_device->getString(GfxStringName::Version)); + ES_LOG_INFO("[Diagnose] GL Renderer: {}", g_device->getString(GfxStringName::Renderer)); + ES_LOG_INFO("[Diagnose] GL Vendor: {}", g_device->getString(GfxStringName::Vendor)); + ES_LOG_INFO("[Diagnose] GLSL Version: {}", g_device->getString(GfxStringName::ShadingLanguageVersion)); ES_LOG_INFO("[Diagnose] Stored viewport: {}x{}", g_viewportWidth, g_viewportHeight); + ES_LOG_INFO("[Diagnose] Max texture units: {}", g_device->getInt(GfxIntParam::MaxTextureImageUnits)); + ES_LOG_INFO("[Diagnose] Max vertex attribs: {}", g_device->getInt(GfxIntParam::MaxVertexAttribs)); - GLint maxTextureUnits; - glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureUnits); - ES_LOG_INFO("[Diagnose] Max texture units: {}", maxTextureUnits); - - GLint maxAttribs; - glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxAttribs); - ES_LOG_INFO("[Diagnose] Max vertex attribs: {}", maxAttribs); - - while (glGetError() != GL_NO_ERROR) {} + while (g_device->getError() != 0) {} ES_LOG_INFO("[Diagnose] No pending GL errors (cleared)"); } @@ -655,7 +630,7 @@ void renderer_clearAllClipRects() { } void renderer_clearStencil() { - glClearStencil(0); + g_device->setClearStencil(0); g_device->clear(false, false, true); } diff --git a/src/esengine/core/Engine.cpp b/src/esengine/core/Engine.cpp index 7bae9989..0a59103f 100644 --- a/src/esengine/core/Engine.cpp +++ b/src/esengine/core/Engine.cpp @@ -13,20 +13,6 @@ #include "Engine.hpp" #include "Log.hpp" -#ifdef ES_PLATFORM_WEB - #include -#elif defined(__APPLE__) - #include -#else - #ifdef _WIN32 - #include - #endif - #include - #ifndef GL_MAX_TEXTURE_SIZE - #define GL_MAX_TEXTURE_SIZE 0x0D33 - #endif -#endif - namespace esengine { const char* Engine::getPlatformName() { @@ -61,10 +47,4 @@ bool Engine::hasWebGL2() { #endif } -u32 Engine::getMaxTextureSize() { - GLint maxSize = 0; - glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxSize); - return (maxSize > 0) ? static_cast(maxSize) : 2048; -} - } // namespace esengine diff --git a/src/esengine/core/Engine.hpp b/src/esengine/core/Engine.hpp index 54700a8b..3e46696c 100644 --- a/src/esengine/core/Engine.hpp +++ b/src/esengine/core/Engine.hpp @@ -121,12 +121,6 @@ class Engine { */ static bool hasWebGL2(); - /** - * @brief Gets the maximum supported texture size - * @return Maximum width/height in pixels - */ - static u32 getMaxTextureSize(); - Engine() = delete; }; diff --git a/src/esengine/renderer/GLDevice.cpp b/src/esengine/renderer/GLDevice.cpp index a844ba65..3396ce83 100644 --- a/src/esengine/renderer/GLDevice.cpp +++ b/src/esengine/renderer/GLDevice.cpp @@ -685,4 +685,28 @@ u32 GLDevice::getError() { return static_cast(glGetError()); } +std::string GLDevice::getString(GfxStringName name) { + GLenum e = GL_VERSION; + switch (name) { + case GfxStringName::Version: e = GL_VERSION; break; + case GfxStringName::Renderer: e = GL_RENDERER; break; + case GfxStringName::Vendor: e = GL_VENDOR; break; + case GfxStringName::ShadingLanguageVersion: e = GL_SHADING_LANGUAGE_VERSION; break; + } + const char* s = reinterpret_cast(glGetString(e)); + return s ? std::string(s) : std::string(); +} + +i32 GLDevice::getInt(GfxIntParam name) { + GLenum e = GL_MAX_TEXTURE_SIZE; + switch (name) { + case GfxIntParam::MaxTextureSize: e = GL_MAX_TEXTURE_SIZE; break; + case GfxIntParam::MaxTextureImageUnits: e = GL_MAX_TEXTURE_IMAGE_UNITS; break; + case GfxIntParam::MaxVertexAttribs: e = GL_MAX_VERTEX_ATTRIBS; break; + } + GLint v = 0; + glGetIntegerv(e, &v); + return static_cast(v); +} + } // namespace esengine diff --git a/src/esengine/renderer/GLDevice.hpp b/src/esengine/renderer/GLDevice.hpp index 1a1011f2..fbe9c1ac 100644 --- a/src/esengine/renderer/GLDevice.hpp +++ b/src/esengine/renderer/GLDevice.hpp @@ -118,6 +118,8 @@ class GLDevice final : public GfxDevice { void setWireframe(bool enabled) override; u32 getError() override; + std::string getString(GfxStringName name) override; + i32 getInt(GfxIntParam name) override; }; } // namespace esengine diff --git a/src/esengine/renderer/GfxDevice.hpp b/src/esengine/renderer/GfxDevice.hpp index fdf3b795..6e28ab2b 100644 --- a/src/esengine/renderer/GfxDevice.hpp +++ b/src/esengine/renderer/GfxDevice.hpp @@ -320,6 +320,12 @@ class GfxDevice { /** @brief Queries the last error */ virtual u32 getError() = 0; + + /** @brief Queries a backend identification string (diagnostics) */ + virtual std::string getString(GfxStringName name) = 0; + + /** @brief Queries a backend integer capability/limit */ + virtual i32 getInt(GfxIntParam name) = 0; }; } // namespace esengine diff --git a/src/esengine/renderer/GfxEnums.hpp b/src/esengine/renderer/GfxEnums.hpp index e1eb7953..568b4e17 100644 --- a/src/esengine/renderer/GfxEnums.hpp +++ b/src/esengine/renderer/GfxEnums.hpp @@ -90,6 +90,25 @@ enum class GfxAttachment : u8 { DepthStencil, }; +// ============================================================================= +// Backend Queries (diagnostics / capabilities) +// ============================================================================= + +/** @brief Backend identification strings. */ +enum class GfxStringName : u8 { + Version, + Renderer, + Vendor, + ShadingLanguageVersion, +}; + +/** @brief Backend integer capabilities/limits. */ +enum class GfxIntParam : u8 { + MaxTextureSize, + MaxTextureImageUnits, + MaxVertexAttribs, +}; + // ============================================================================= // Shader Program Creation // ============================================================================= diff --git a/tests/renderer/MockGfxDevice.hpp b/tests/renderer/MockGfxDevice.hpp index 4fc719be..258a36cb 100644 --- a/tests/renderer/MockGfxDevice.hpp +++ b/tests/renderer/MockGfxDevice.hpp @@ -134,6 +134,8 @@ struct MockGfxDevice final : GfxDevice { void setWireframe(bool) override {} u32 getError() override { return 0; } + std::string getString(GfxStringName) override { return {}; } + i32 getInt(GfxIntParam) override { return 16; } }; } // namespace esengine diff --git a/tools/check-gl-boundary.mjs b/tools/check-gl-boundary.mjs new file mode 100644 index 00000000..ace11eac --- /dev/null +++ b/tools/check-gl-boundary.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +// ============================================================================= +// GL boundary guard (RC5) +// +// Enforces the keystone invariant: raw OpenGL/WebGL calls (`glXxx(...)`) may +// appear ONLY in the single backend implementation (renderer/GLDevice.cpp). +// Every other translation unit must reach the GPU through GfxDevice/StateTracker. +// +// Run: node tools/check-gl-boundary.mjs (exit 1 on violation) +// ============================================================================= + +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const ROOT = 'src/esengine'; + +// The one file allowed to call gl* — the concrete GfxDevice backend. +const ALLOWED = new Set([ + 'src/esengine/renderer/GLDevice.cpp', +]); + +// A gl call: `gl` + uppercase letter + identifier + `(`. Does not match `glm::` +// (lowercase m) or `GL_CONSTANT` macros. +const GL_CALL = /\bgl[A-Z]\w*\s*\(/; +const SOURCE_EXT = /\.(cpp|cc|cxx|hpp|hxx|h)$/; + +function walk(dir, out = []) { + for (const entry of readdirSync(dir)) { + const p = join(dir, entry); + if (statSync(p).isDirectory()) walk(p, out); + else if (SOURCE_EXT.test(entry)) out.push(p); + } + return out; +} + +const violations = []; +for (const file of walk(ROOT)) { + const rel = file.replace(/\\/g, '/'); + if (ALLOWED.has(rel)) continue; + const lines = readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, i) => { + if (GL_CALL.test(line)) { + violations.push(` ${rel}:${i + 1}: ${line.trim()}`); + } + }); +} + +if (violations.length > 0) { + console.error('GL boundary violation — raw gl* calls must live only in GLDevice.cpp:\n'); + console.error(violations.join('\n')); + console.error(`\n${violations.length} violation(s). Route GPU work through GfxDevice / StateTracker.`); + process.exit(1); +} + +console.log('GL boundary OK: no gl* calls outside GLDevice.cpp.');