From e6ddb52566622c34821454e5641a71bb1bdcda31 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 6 Sep 2026 23:17:08 -0400 Subject: [PATCH 1/9] Widen test coverage --- .github/workflows/fedora.yml | 14 ++++---- .github/workflows/offscreen.yml | 19 +++++++--- CMakeLists.txt | 62 +++++++++++++++++++++++++++------ 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/.github/workflows/fedora.yml b/.github/workflows/fedora.yml index 3f3bc9e..5a4b6cc 100644 --- a/.github/workflows/fedora.yml +++ b/.github/workflows/fedora.yml @@ -23,7 +23,7 @@ jobs: container: fedora:latest steps: - - run: yum install -y g++ cmake git glfw-devel + - run: dnf install -y gcc-c++ cmake git glfw-devel mesa-libGL-devel mesa-libEGL-devel mesa-dri-drivers xorg-x11-server-Xvfb - uses: actions/checkout@v6 with: @@ -44,14 +44,16 @@ jobs: - name: Test working-directory: /__w/offscreen/offscreen/build - # Execute tests defined by the CMake configuration. - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest -C ${{env.BUILD_TYPE}} + env: + LIBGL_ALWAYS_SOFTWARE: "1" + run: xvfb-run ctest --output-on-failure -C ${{env.BUILD_TYPE}} - name: Upload Test Results uses: actions/upload-artifact@v6 if: ${{ always() }} with: - name: Test Results(${{ matrix.os }}) + name: Test Results(Fedora) path: | - build/Testing/Temporary/LastTest.log \ No newline at end of file + build/Testing/Temporary/LastTest.log + build/out.png + build/out_320x240.png diff --git a/.github/workflows/offscreen.yml b/.github/workflows/offscreen.yml index 771a842..f09ceeb 100644 --- a/.github/workflows/offscreen.yml +++ b/.github/workflows/offscreen.yml @@ -21,8 +21,9 @@ jobs: build: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, macos-14] + os: [ubuntu-latest, macos-latest, macos-14, windows-latest] steps: - uses: actions/checkout@v6 @@ -31,7 +32,7 @@ jobs: - uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libglvnd-dev libglfw3-dev libglu1-mesa-dev + packages: libglvnd-dev libglfw3-dev libglu1-mesa-dev libegl1-mesa-dev libgbm-dev xvfb version: 1.0 if: runner.os == 'Linux' - run: brew install glfw @@ -52,9 +53,15 @@ jobs: - name: Test working-directory: ${{github.workspace}}/build - # Execute tests defined by the CMake configuration. - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest -C ${{env.BUILD_TYPE}} + env: + LIBGL_ALWAYS_SOFTWARE: "1" + run: | + if [ "$RUNNER_OS" = "Linux" ]; then + xvfb-run ctest --output-on-failure -C ${{env.BUILD_TYPE}} + else + ctest --output-on-failure -C ${{env.BUILD_TYPE}} + fi + shell: bash - name: Upload Test Results uses: actions/upload-artifact@v6 @@ -64,3 +71,5 @@ jobs: path: | build/Testing/Temporary/LastTest.log build/out.png + build/out_320x240.png + diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a2b676..f489648 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,13 +67,24 @@ if(OpenGL_GLX_FOUND) endif() if(ENABLE_GLFW) - find_package(glfw3 REQUIRED) - target_link_libraries(offscreen glfw) - target_compile_definitions(offscreen PRIVATE ENABLE_GLFW) + find_package(glfw3 QUIET) + if(TARGET glfw OR TARGET glfw3 OR glfw3_FOUND) + if(TARGET glfw) + target_link_libraries(offscreen glfw) + else() + target_link_libraries(offscreen glfw3) + endif() + target_compile_definitions(offscreen PRIVATE ENABLE_GLFW) + else() + message(STATUS "GLFW not found, disabling on-screen GLFW backend") + set(ENABLE_GLFW OFF) + endif() +endif() + +if(CMAKE_DL_LIBS) + target_link_libraries(offscreen ${CMAKE_DL_LIBS}) endif() -# Needed for Raspberry pi: -target_link_libraries(offscreen dl) if(APPLE) set(HAS_NSOPENGL TRUE) @@ -138,22 +149,51 @@ add_test(NAME default_run COMMAND offscreen) add_test(NAME fails_on_empty_context_arg COMMAND offscreen --context) set_property(TEST fails_on_empty_context_arg PROPERTY WILL_FAIL true) +add_test(NAME fails_on_invalid_context COMMAND offscreen --context nonexistent_backend) +set_property(TEST fails_on_invalid_context PROPERTY WILL_FAIL true) + +add_test(NAME fails_on_immediate_in_core_profile COMMAND offscreen --opengl 3.3 --profile core --mode immediate) +set_property(TEST fails_on_immediate_in_core_profile PROPERTY WILL_FAIL true) + add_test(NAME nullgl_test COMMAND offscreen --context nullgl) add_test(NAME will_save_framebuffer COMMAND offscreen -o out.png) add_test(NAME check_file_exists COMMAND ${CMAKE_COMMAND} -E cat out.png) set_tests_properties(check_file_exists PROPERTIES DEPENDS will_save_framebuffer) +add_test(NAME custom_dimensions_save COMMAND offscreen --width 320 --height 240 -o out_320x240.png) +add_test(NAME check_custom_file_exists COMMAND ${CMAKE_COMMAND} -E cat out_320x240.png) +set_tests_properties(check_custom_file_exists PROPERTIES DEPENDS custom_dimensions_save) if(APPLE) -add_test(NAME cgl_opengl2_immediate COMMAND offscreen --context cgl --opengl 2 --mode immediate) -add_test(NAME cgl_opengl2_modern COMMAND offscreen --context cgl --opengl 2 --mode modern) -add_test(NAME cgl_opengl3.2_core COMMAND offscreen --context cgl --opengl 3.2 --profile core) -add_test(NAME nsopengl_opengl2_immediate COMMAND offscreen --context nsopengl --opengl 2 --mode immediate) -add_test(NAME nsopengl_opengl2_modern COMMAND offscreen --context nsopengl --opengl 2 --mode modern) -add_test(NAME nsopengl_opengl3.2_core COMMAND offscreen --context nsopengl --opengl 3.2 --profile core) + add_test(NAME cgl_opengl2_immediate COMMAND offscreen --context cgl --opengl 2 --mode immediate) + add_test(NAME cgl_opengl2_modern COMMAND offscreen --context cgl --opengl 2 --mode modern) + add_test(NAME cgl_opengl3.2_core COMMAND offscreen --context cgl --opengl 3.2 --profile core) + add_test(NAME nsopengl_opengl2_immediate COMMAND offscreen --context nsopengl --opengl 2 --mode immediate) + add_test(NAME nsopengl_opengl2_modern COMMAND offscreen --context nsopengl --opengl 2 --mode modern) + add_test(NAME nsopengl_opengl3.2_core COMMAND offscreen --context nsopengl --opengl 3.2 --profile core) endif(APPLE) +if(HAS_EGL) + add_test(NAME egl_opengl2_modern COMMAND offscreen --context egl --opengl 2 --mode modern) + add_test(NAME egl_gles2 COMMAND offscreen --context egl --gles 2) + add_test(NAME egl_opengl3_core COMMAND offscreen --context egl --opengl 3.3 --profile core) +endif() + +if(HAS_GLX) + add_test(NAME glx_opengl2_immediate COMMAND offscreen --context glx --opengl 2 --mode immediate) + add_test(NAME glx_opengl2_modern COMMAND offscreen --context glx --opengl 2 --mode modern) + add_test(NAME glx_opengl3_core COMMAND offscreen --context glx --opengl 3.3 --profile core) +endif() + +if(WIN32) + add_test(NAME wgl_opengl2_immediate COMMAND offscreen --context wgl --opengl 2 --mode immediate) + add_test(NAME wgl_opengl2_modern COMMAND offscreen --context wgl --opengl 2 --mode modern) + add_test(NAME wgl_opengl3_core COMMAND offscreen --context wgl --opengl 3.3 --profile core) + add_test(NAME wgl_opengl3_compat COMMAND offscreen --context wgl --opengl 3.3 --profile compatibility) +endif(WIN32) + + message(STATUS " ") message(STATUS "====================================") message(STATUS "Build Configuration Summary") From f612c419d33b067a5cd2e8b25abdc8b0ae8d5183 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Sun, 6 Sep 2026 23:51:51 -0400 Subject: [PATCH 2/9] Fix MSVC build and GLES FBO support on Linux --- src/FBO.cc | 12 +++++++++++- src/OffscreenContextWGL.cc | 31 +++++++++++++++---------------- src/main.cc | 27 +++++++++++++++------------ src/system-gl.h | 5 +++-- 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/FBO.cc b/src/FBO.cc index c575e7a..b4ced1c 100644 --- a/src/FBO.cc +++ b/src/FBO.cc @@ -54,16 +54,26 @@ bool checkFBOStatus() { } // namespace std::unique_ptr createFBO(int width, int height) { +#ifdef USE_GLAD + if (GLAD_GL_VERSION_3_0 || GLAD_GL_ES_VERSION_2_0 || hasGLExtension(ARB_framebuffer_object)) { + return std::make_unique(width, height, /*useEXT*/ false); + } else if (hasGLExtension(EXT_framebuffer_object)) { + return std::make_unique(width, height, /*useEXT*/ true); + } +#else if (hasGLExtension(ARB_framebuffer_object)) { return std::make_unique(width, height, /*useEXT*/ false); } else if (hasGLExtension(EXT_framebuffer_object)) { return std::make_unique(width, height, /*useEXT*/ true); - } else { + } +#endif + else { std::cerr << "Framebuffer Objects not supported" << std::endl; return nullptr; } } + FBO::FBO(int width, int height, bool useEXT) : width_(width), height_(height), use_ext_(useEXT) { // Generate and bind FBO GL_CHECK(glGenFramebuffers(1, &this->fbo_id_)); diff --git a/src/OffscreenContextWGL.cc b/src/OffscreenContextWGL.cc index bfac4a7..6cc8909 100644 --- a/src/OffscreenContextWGL.cc +++ b/src/OffscreenContextWGL.cc @@ -49,12 +49,11 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t { auto ctx = std::make_shared(width, height); - WNDCLASSEX wndClass = { - .cbSize = sizeof(WNDCLASSEX), - .style = CS_OWNDC, - .lpfnWndProc = &DefWindowProc, - .lpszClassName = "OffscreenClass" - }; + WNDCLASSEX wndClass = {}; + wndClass.cbSize = sizeof(WNDCLASSEX); + wndClass.style = CS_OWNDC; + wndClass.lpfnWndProc = &DefWindowProc; + wndClass.lpszClassName = "OffscreenClass"; // FIXME: Check for ERROR_CLASS_ALREADY_EXISTS ? RegisterClassEx(&wndClass); // Create the window. Position and size it. @@ -63,16 +62,16 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, 0, 0); ctx->devContext = GetDC(ctx->window); - PIXELFORMATDESCRIPTOR pixelFormatDesc = { - .nSize = sizeof(PIXELFORMATDESCRIPTOR), - .nVersion = 1, - // FIXME: Can we remove PFD_DOUBLEBUFFER for offscreen rendering? - .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, - .iPixelType = PFD_TYPE_RGBA, - .cColorBits = 32, - .cDepthBits = 24, - .cStencilBits = 8 - }; + PIXELFORMATDESCRIPTOR pixelFormatDesc = {}; + pixelFormatDesc.nSize = sizeof(PIXELFORMATDESCRIPTOR); + pixelFormatDesc.nVersion = 1; + // FIXME: Can we remove PFD_DOUBLEBUFFER for offscreen rendering? + pixelFormatDesc.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pixelFormatDesc.iPixelType = PFD_TYPE_RGBA; + pixelFormatDesc.cColorBits = 32; + pixelFormatDesc.cDepthBits = 24; + pixelFormatDesc.cStencilBits = 8; + int pixelFormat = ChoosePixelFormat(ctx->devContext, &pixelFormatDesc); SetPixelFormat(ctx->devContext, pixelFormat, &pixelFormatDesc); // FIXME: Use wglChoosePixelFormatARB() if appropriate diff --git a/src/main.cc b/src/main.cc index e879181..b7001fd 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,9 +1,12 @@ #include -#include +#include +#include #include +#include #include +#include #include -#include + #ifdef USE_GLAD #define GLAD_GL_IMPLEMENTATION @@ -164,16 +167,16 @@ int main(int argc, char *argv[]) std::transform(argContextProvider.begin(), argContextProvider.end(), argContextProvider.begin(), ::tolower); - OffscreenContextFactory::ContextAttributes attrib = { - .width = argWidth, - .height = argHeight, - .majorGLVersion = requestMajor, - .minorGLVersion = requestMinor, - .gles = requestGLES, - .compatibilityProfile = argProfile == "compatibility", - .gpu = argGPU, - .invisible = argInvisible, - }; + OffscreenContextFactory::ContextAttributes attrib{}; + attrib.width = argWidth; + attrib.height = argHeight; + attrib.majorGLVersion = requestMajor; + attrib.minorGLVersion = requestMinor; + attrib.gles = requestGLES; + attrib.compatibilityProfile = (argProfile == "compatibility"); + attrib.gpu = argGPU; + attrib.invisible = argInvisible; + ctx = OffscreenContextFactory::create(argContextProvider, attrib); if (!ctx) { std::cerr << "Error: Unable to create GL context" << std::endl; diff --git a/src/system-gl.h b/src/system-gl.h index e6771b2..13e172b 100644 --- a/src/system-gl.h +++ b/src/system-gl.h @@ -42,8 +42,9 @@ bool lookupGLExtension(const char *ext); #endif #ifdef DEBUG - #define GL_CHECK(stmt) stmt; glCheck(#stmt, __FILE__, __LINE__) + #define GL_CHECK(...) __VA_ARGS__; glCheck(#__VA_ARGS__, __FILE__, __LINE__) #else - #define GL_CHECK(stmt) stmt + #define GL_CHECK(...) __VA_ARGS__ #endif + From fff16881663847385323fb91a9f428186fcb0c30 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 7 Sep 2026 00:03:05 -0400 Subject: [PATCH 3/9] WGL robust error handling and Mesa for Windows CI --- .github/workflows/offscreen.yml | 4 +++ src/OffscreenContextWGL.cc | 58 +++++++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/.github/workflows/offscreen.yml b/.github/workflows/offscreen.yml index f09ceeb..bef69b4 100644 --- a/.github/workflows/offscreen.yml +++ b/.github/workflows/offscreen.yml @@ -37,6 +37,10 @@ jobs: if: runner.os == 'Linux' - run: brew install glfw if: runner.os == 'macOS' + - name: Install Mesa (Windows) + uses: ssciwr/setup-mesa-dist-win@v3 + if: runner.os == 'Windows' + - name: Setup tmate session uses: mxschmitt/action-tmate@v3 diff --git a/src/OffscreenContextWGL.cc b/src/OffscreenContextWGL.cc index 6cc8909..b3b97ce 100644 --- a/src/OffscreenContextWGL.cc +++ b/src/OffscreenContextWGL.cc @@ -53,6 +53,7 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t wndClass.cbSize = sizeof(WNDCLASSEX); wndClass.style = CS_OWNDC; wndClass.lpfnWndProc = &DefWindowProc; + wndClass.hInstance = GetModuleHandle(nullptr); wndClass.lpszClassName = "OffscreenClass"; // FIXME: Check for ERROR_CLASS_ALREADY_EXISTS ? RegisterClassEx(&wndClass); @@ -60,7 +61,15 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t // Style the window and remove the caption bar (WS_POPUP) ctx->window = CreateWindowEx(0, "OffscreenClass", "offscreen", WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, 0, 0); + if (!ctx->window) { + std::cerr << "CreateWindowEx() failed: " << GetLastError() << std::endl; + return nullptr; + } ctx->devContext = GetDC(ctx->window); + if (!ctx->devContext) { + std::cerr << "GetDC() failed: " << GetLastError() << std::endl; + return nullptr; + } PIXELFORMATDESCRIPTOR pixelFormatDesc = {}; pixelFormatDesc.nSize = sizeof(PIXELFORMATDESCRIPTOR); @@ -73,8 +82,14 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t pixelFormatDesc.cStencilBits = 8; int pixelFormat = ChoosePixelFormat(ctx->devContext, &pixelFormatDesc); - SetPixelFormat(ctx->devContext, pixelFormat, &pixelFormatDesc); - // FIXME: Use wglChoosePixelFormatARB() if appropriate + if (!pixelFormat) { + std::cerr << "ChoosePixelFormat() failed: " << GetLastError() << std::endl; + return nullptr; + } + if (!SetPixelFormat(ctx->devContext, pixelFormat, &pixelFormatDesc)) { + std::cerr << "SetPixelFormat() failed: " << GetLastError() << std::endl; + return nullptr; + } const auto tmpRenderContext = wglCreateContext(ctx->devContext); if (tmpRenderContext == nullptr) { @@ -86,21 +101,36 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t wglDeleteContext(tmpRenderContext); }); - wglMakeCurrent(ctx->devContext, tmpRenderContext); + if (!wglMakeCurrent(ctx->devContext, tmpRenderContext)) { + std::cerr << "wglMakeCurrent() failed: " << GetLastError() << std::endl; + return nullptr; + } + gladLoaderLoadWGL(ctx->devContext); - int attributes[] = { - WGL_CONTEXT_MAJOR_VERSION_ARB, static_cast(majorGLVersion), - WGL_CONTEXT_MINOR_VERSION_ARB, static_cast(minorGLVersion), - WGL_CONTEXT_PROFILE_MASK_ARB, - compatibilityProfile ? WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB : WGL_CONTEXT_CORE_PROFILE_BIT_ARB, - 0 - }; - ctx->renderContext = wglCreateContextAttribsARB(ctx->devContext, nullptr, attributes); - if (ctx->renderContext == nullptr) { - std::cerr << "wglCreateContextAttribsARB() failed: " << GetLastError() << std::endl; - return nullptr; + if (wglCreateContextAttribsARB) { + int attributes[] = { + WGL_CONTEXT_MAJOR_VERSION_ARB, static_cast(majorGLVersion), + WGL_CONTEXT_MINOR_VERSION_ARB, static_cast(minorGLVersion), + WGL_CONTEXT_PROFILE_MASK_ARB, + compatibilityProfile ? WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB : WGL_CONTEXT_CORE_PROFILE_BIT_ARB, + 0 + }; + ctx->renderContext = wglCreateContextAttribsARB(ctx->devContext, nullptr, attributes); + if (ctx->renderContext == nullptr) { + std::cerr << "wglCreateContextAttribsARB() failed: " << GetLastError() << std::endl; + return nullptr; + } + } else { + if (majorGLVersion > 2) { + std::cerr << "wglCreateContextAttribsARB() not available, cannot create modern OpenGL context" << std::endl; + return nullptr; + } + // Fall back to the legacy context + ctx->renderContext = tmpRenderContext; + guard.dismiss(); } return ctx; + } From b74d1f0ddc79017e966ce702aded727aa8116c21 Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 7 Sep 2026 00:11:45 -0400 Subject: [PATCH 4/9] Use designated initializer for ContextAttributes in main.cc --- CMakeLists.txt | 10 ++++++++-- src/main.cc | 20 +++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f489648..4ae2038 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,8 +141,14 @@ set(SRCS ) target_sources(offscreen PRIVATE ${SRCS}) target_include_directories(offscreen PUBLIC "${CMAKE_SOURCE_DIR}/src") -# Using C++17 as my RaspberryPi runs gcc=8.3 which doesn't implement -set_property(TARGET offscreen PROPERTY CXX_STANDARD 17) +if(MSVC) + # MSVC requires /std:c++20 for designated initializers + set_property(TARGET offscreen PROPERTY CXX_STANDARD 20) +else() + # Using C++17 as my RaspberryPi runs gcc=8.3 which doesn't implement + set_property(TARGET offscreen PROPERTY CXX_STANDARD 17) +endif() + enable_testing() add_test(NAME default_run COMMAND offscreen) diff --git a/src/main.cc b/src/main.cc index b7001fd..375b262 100644 --- a/src/main.cc +++ b/src/main.cc @@ -167,15 +167,17 @@ int main(int argc, char *argv[]) std::transform(argContextProvider.begin(), argContextProvider.end(), argContextProvider.begin(), ::tolower); - OffscreenContextFactory::ContextAttributes attrib{}; - attrib.width = argWidth; - attrib.height = argHeight; - attrib.majorGLVersion = requestMajor; - attrib.minorGLVersion = requestMinor; - attrib.gles = requestGLES; - attrib.compatibilityProfile = (argProfile == "compatibility"); - attrib.gpu = argGPU; - attrib.invisible = argInvisible; + OffscreenContextFactory::ContextAttributes attrib = { + .width = argWidth, + .height = argHeight, + .majorGLVersion = requestMajor, + .minorGLVersion = requestMinor, + .gles = requestGLES, + .compatibilityProfile = (argProfile == "compatibility"), + .gpu = argGPU, + .invisible = argInvisible, + }; + ctx = OffscreenContextFactory::create(argContextProvider, attrib); if (!ctx) { From f54c63a9888e3561a179497b7dd9acb4261afedd Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 7 Sep 2026 00:18:41 -0400 Subject: [PATCH 5/9] Wrap OffscreenContextWGL in _WIN32 and use designated initializers --- src/OffscreenContextWGL.cc | 44 +++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/OffscreenContextWGL.cc b/src/OffscreenContextWGL.cc index b3b97ce..3b36cee 100644 --- a/src/OffscreenContextWGL.cc +++ b/src/OffscreenContextWGL.cc @@ -1,5 +1,7 @@ #include "OffscreenContextWGL.h" +#ifdef _WIN32 + #include #include #include @@ -49,12 +51,13 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t { auto ctx = std::make_shared(width, height); - WNDCLASSEX wndClass = {}; - wndClass.cbSize = sizeof(WNDCLASSEX); - wndClass.style = CS_OWNDC; - wndClass.lpfnWndProc = &DefWindowProc; - wndClass.hInstance = GetModuleHandle(nullptr); - wndClass.lpszClassName = "OffscreenClass"; + WNDCLASSEX wndClass = { + .cbSize = sizeof(WNDCLASSEX), + .style = CS_OWNDC, + .lpfnWndProc = &DefWindowProc, + .hInstance = GetModuleHandle(nullptr), + .lpszClassName = "OffscreenClass" + }; // FIXME: Check for ERROR_CLASS_ALREADY_EXISTS ? RegisterClassEx(&wndClass); // Create the window. Position and size it. @@ -71,15 +74,16 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t return nullptr; } - PIXELFORMATDESCRIPTOR pixelFormatDesc = {}; - pixelFormatDesc.nSize = sizeof(PIXELFORMATDESCRIPTOR); - pixelFormatDesc.nVersion = 1; - // FIXME: Can we remove PFD_DOUBLEBUFFER for offscreen rendering? - pixelFormatDesc.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; - pixelFormatDesc.iPixelType = PFD_TYPE_RGBA; - pixelFormatDesc.cColorBits = 32; - pixelFormatDesc.cDepthBits = 24; - pixelFormatDesc.cStencilBits = 8; + PIXELFORMATDESCRIPTOR pixelFormatDesc = { + .nSize = sizeof(PIXELFORMATDESCRIPTOR), + .nVersion = 1, + // FIXME: Can we remove PFD_DOUBLEBUFFER for offscreen rendering? + .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, + .iPixelType = PFD_TYPE_RGBA, + .cColorBits = 32, + .cDepthBits = 24, + .cStencilBits = 8 + }; int pixelFormat = ChoosePixelFormat(ctx->devContext, &pixelFormatDesc); if (!pixelFormat) { @@ -132,5 +136,15 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t } return ctx; +} + +#else // !_WIN32 +std::shared_ptr CreateOffscreenContextWGL(size_t /*width*/, size_t /*height*/, + size_t /*majorGLVersion*/, size_t /*minorGLVersion*/, bool /*compatibilityProfile*/) +{ + return nullptr; } + +#endif // _WIN32 + From 4edfa32460a7e57e5161b54ce517e77f1a82083c Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 7 Sep 2026 00:26:52 -0400 Subject: [PATCH 6/9] Remove unnecessary _WIN32 guards from OffscreenContextWGL.cc --- src/OffscreenContextWGL.cc | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/OffscreenContextWGL.cc b/src/OffscreenContextWGL.cc index 3b36cee..74153f2 100644 --- a/src/OffscreenContextWGL.cc +++ b/src/OffscreenContextWGL.cc @@ -1,7 +1,5 @@ #include "OffscreenContextWGL.h" -#ifdef _WIN32 - #include #include #include @@ -138,13 +136,4 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t return ctx; } -#else // !_WIN32 - -std::shared_ptr CreateOffscreenContextWGL(size_t /*width*/, size_t /*height*/, - size_t /*majorGLVersion*/, size_t /*minorGLVersion*/, bool /*compatibilityProfile*/) -{ - return nullptr; -} - -#endif // _WIN32 From 261c7275d809ee505037b20e19b5e8fd1abd2fce Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 7 Sep 2026 00:31:45 -0400 Subject: [PATCH 7/9] Unify GL version detection for FBO support between GLAD and non-GLAD --- src/FBO.cc | 14 +++----------- src/system-gl.cc | 39 +++++++++++++++++++++++++++++++-------- src/system-gl.h | 5 +++++ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/FBO.cc b/src/FBO.cc index b4ced1c..e2dad89 100644 --- a/src/FBO.cc +++ b/src/FBO.cc @@ -54,26 +54,18 @@ bool checkFBOStatus() { } // namespace std::unique_ptr createFBO(int width, int height) { -#ifdef USE_GLAD - if (GLAD_GL_VERSION_3_0 || GLAD_GL_ES_VERSION_2_0 || hasGLExtension(ARB_framebuffer_object)) { + if (hasGLVersion3() || hasGLESVersion2() || hasGLExtension(ARB_framebuffer_object)) { return std::make_unique(width, height, /*useEXT*/ false); } else if (hasGLExtension(EXT_framebuffer_object)) { return std::make_unique(width, height, /*useEXT*/ true); - } -#else - if (hasGLExtension(ARB_framebuffer_object)) { - return std::make_unique(width, height, /*useEXT*/ false); - } else if (hasGLExtension(EXT_framebuffer_object)) { - return std::make_unique(width, height, /*useEXT*/ true); - } -#endif - else { + } else { std::cerr << "Framebuffer Objects not supported" << std::endl; return nullptr; } } + FBO::FBO(int width, int height, bool useEXT) : width_(width), height_(height), use_ext_(useEXT) { // Generate and bind FBO GL_CHECK(glGenFramebuffers(1, &this->fbo_id_)); diff --git a/src/system-gl.cc b/src/system-gl.cc index 5d7cfb4..79481de 100644 --- a/src/system-gl.cc +++ b/src/system-gl.cc @@ -1,5 +1,7 @@ #include "system-gl.h" +#include +#include #include #include #include @@ -7,21 +9,30 @@ namespace { std::set glExtensions; +int glMajorVersion = 0; +bool glIsGLES = false; +void queryGLVersionIfNeeded() { + if (glMajorVersion > 0) return; + const char *v = reinterpret_cast(glGetString(GL_VERSION)); + if (!v) return; + if (std::strncmp(v, "OpenGL ES ", 10) == 0) { + glIsGLES = true; + v += 10; + } + std::sscanf(v, "%d", &glMajorVersion); } +} // namespace + #ifndef USE_GLAD void initGLExtensions(int major, int minor, bool gles) { glExtensions.clear(); - // Framebuffer Objects were promoted to core functionality in OpenGL 3.0 and GLES 2.0. - // Core Profile drivers (e.g. macOS Core Profile, Mesa) do not list promoted core features - // in the GL_EXTENSIONS string list, so we explicitly insert GL_ARB_framebuffer_object here - // to maintain compatibility with hasGLExtension(ARB_framebuffer_object) checks in non-GLAD builds. - if (major >= 3 || (gles && major >= 2)) { - glExtensions.insert("GL_ARB_framebuffer_object"); - } + glMajorVersion = major; + glIsGLES = gles; + if (major == 2 && !gles) { const char *extensions = reinterpret_cast(glGetString(GL_EXTENSIONS)); if (extensions) { @@ -44,10 +55,22 @@ void initGLExtensions(int major, int minor, bool gles) } } - bool lookupGLExtension(const char *ext) { return glExtensions.find(ext) != glExtensions.end(); } +bool hasGLVersion3() +{ + queryGLVersionIfNeeded(); + return !glIsGLES && glMajorVersion >= 3; +} + +bool hasGLESVersion2() +{ + queryGLVersionIfNeeded(); + return glIsGLES && glMajorVersion >= 2; +} + #endif + diff --git a/src/system-gl.h b/src/system-gl.h index 13e172b..cf8b319 100644 --- a/src/system-gl.h +++ b/src/system-gl.h @@ -35,12 +35,17 @@ void glCheck(const char *stmt, const char *file, int line) #ifdef USE_GLAD #define hasGLExtension(ext) GLAD_GL_##ext +#define hasGLVersion3() (GLAD_GL_VERSION_3_0 != 0) +#define hasGLESVersion2() (GLAD_GL_ES_VERSION_2_0 != 0) #else void initGLExtensions(int major, int minor, bool gles); bool lookupGLExtension(const char *ext); #define hasGLExtension(ext) lookupGLExtension("GL_" #ext) +bool hasGLVersion3(); +bool hasGLESVersion2(); #endif + #ifdef DEBUG #define GL_CHECK(...) __VA_ARGS__; glCheck(#__VA_ARGS__, __FILE__, __LINE__) #else From 3a1fc71407e6cf9d2983bdc077c7873db550fe6d Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 7 Sep 2026 00:42:50 -0400 Subject: [PATCH 8/9] Add ENABLE_EGL and ENABLE_GLX options and test both EGL and GLX fully --- CMakeLists.txt | 23 ++++++++++++++++++----- src/main.cc | 13 ++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ae2038..d9c09bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,8 @@ endif() option(USE_GLAD "Use GLAD for OpenGL function wrangling" ${USE_GLAD_DEFAULT}) option(ENABLE_GLFW "Enable on-screen rendering using glfw" ON) +option(ENABLE_EGL "Enable EGL offscreen context" ON) +option(ENABLE_GLX "Enable GLX offscreen context" ON) set(HAS_GLVND FALSE) set(HAS_GLX FALSE) @@ -37,7 +39,7 @@ else() target_link_libraries(offscreen OpenGL::GL) endif() target_link_libraries(offscreen OpenGL::GLU) -if (OpenGL_EGL_FOUND) +if (ENABLE_EGL AND OpenGL_EGL_FOUND) if (TARGET OpenGL::EGL) # GLVND systems target_link_libraries(offscreen OpenGL::EGL) @@ -53,7 +55,7 @@ if (OpenGL_EGL_FOUND) target_link_libraries(offscreen GBM::GBM) endif() endif() -if(OpenGL_GLX_FOUND) +if(ENABLE_GLX AND OpenGL_GLX_FOUND) find_package(X11 REQUIRED) if(X11_FOUND) if(TARGET OpenGL::OpenGL) @@ -62,10 +64,10 @@ if(OpenGL_GLX_FOUND) endif() set(HAS_GLX TRUE) target_link_libraries(offscreen X11::X11) - target_compile_definitions(offscreen PRIVATE ENABLE_GLX) endif() endif() + if(ENABLE_GLFW) find_package(glfw3 QUIET) if(TARGET glfw OR TARGET glfw3 OR glfw3_FOUND) @@ -107,16 +109,17 @@ if(HAS_EGL) src/OffscreenContextEGL.cc src/egl_utils.cc ) - add_compile_definitions(HAS_EGL) + add_compile_definitions(HAS_EGL ENABLE_EGL) endif() if(HAS_GLX) set(SRCS_GLX src/OffscreenContextGLX.cc ) - add_compile_definitions(HAS_GLX) + add_compile_definitions(HAS_GLX ENABLE_GLX) endif() + if(ENABLE_GLFW) set(SRCS_GLFW src/GLFWContext.cc) endif() @@ -181,17 +184,27 @@ if(APPLE) endif(APPLE) if(HAS_EGL) + add_test(NAME egl_opengl2_immediate COMMAND offscreen --context egl --opengl 2 --mode immediate) add_test(NAME egl_opengl2_modern COMMAND offscreen --context egl --opengl 2 --mode modern) add_test(NAME egl_gles2 COMMAND offscreen --context egl --gles 2) add_test(NAME egl_opengl3_core COMMAND offscreen --context egl --opengl 3.3 --profile core) + add_test(NAME egl_opengl3_compat COMMAND offscreen --context egl --opengl 3.3 --profile compatibility) + add_test(NAME egl_save_framebuffer COMMAND offscreen --context egl -o out_egl.png) + add_test(NAME check_egl_file_exists COMMAND ${CMAKE_COMMAND} -E cat out_egl.png) + set_tests_properties(check_egl_file_exists PROPERTIES DEPENDS egl_save_framebuffer) endif() if(HAS_GLX) add_test(NAME glx_opengl2_immediate COMMAND offscreen --context glx --opengl 2 --mode immediate) add_test(NAME glx_opengl2_modern COMMAND offscreen --context glx --opengl 2 --mode modern) add_test(NAME glx_opengl3_core COMMAND offscreen --context glx --opengl 3.3 --profile core) + add_test(NAME glx_opengl3_compat COMMAND offscreen --context glx --opengl 3.3 --profile compatibility) + add_test(NAME glx_save_framebuffer COMMAND offscreen --context glx -o out_glx.png) + add_test(NAME check_glx_file_exists COMMAND ${CMAKE_COMMAND} -E cat out_glx.png) + set_tests_properties(check_glx_file_exists PROPERTIES DEPENDS glx_save_framebuffer) endif() + if(WIN32) add_test(NAME wgl_opengl2_immediate COMMAND offscreen --context wgl --opengl 2 --mode immediate) add_test(NAME wgl_opengl2_modern COMMAND offscreen --context wgl --opengl 2 --mode modern) diff --git a/src/main.cc b/src/main.cc index 375b262..a01fa36 100644 --- a/src/main.cc +++ b/src/main.cc @@ -82,14 +82,25 @@ int main(int argc, char *argv[]) bool argVerbose = false; bool argPrintHelp = false; - std::vector contextProviders = {"egl", "cgl", "nsopengl", "wgl", "nullgl"}; + std::vector contextProviders = {"nullgl"}; +#ifdef __APPLE__ + contextProviders.push_back("cgl"); + contextProviders.push_back("nsopengl"); +#endif +#if HAS_EGL + contextProviders.push_back("egl"); +#endif #ifdef ENABLE_GLX contextProviders.push_back("glx"); #endif +#ifdef _WIN32 + contextProviders.push_back("wgl"); +#endif #ifdef ENABLE_GLFW contextProviders.push_back("glfw"); #endif + std::string joinedProviders = std::accumulate(contextProviders.begin(), contextProviders.end(), std::string(), [](const auto &x, const auto &y) { return x.empty() ? y : x + " | " + y; }); From ca085f9ee3456a0e7f96c84d9770a2b423ff34ab Mon Sep 17 00:00:00 2001 From: Marius Kintel Date: Mon, 7 Sep 2026 00:49:01 -0400 Subject: [PATCH 9/9] Test macOS Intel via macos-13 in CI matrix --- .github/workflows/offscreen.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/offscreen.yml b/.github/workflows/offscreen.yml index bef69b4..e93e87d 100644 --- a/.github/workflows/offscreen.yml +++ b/.github/workflows/offscreen.yml @@ -23,7 +23,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, macos-14, windows-latest] + os: [ubuntu-latest, macos-13, macos-latest, windows-latest] + steps: - uses: actions/checkout@v6