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..e93e87d 100644 --- a/.github/workflows/offscreen.yml +++ b/.github/workflows/offscreen.yml @@ -21,8 +21,10 @@ jobs: build: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, macos-14] + os: [ubuntu-latest, macos-13, macos-latest, windows-latest] + steps: - uses: actions/checkout@v6 @@ -31,11 +33,15 @@ 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 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 @@ -52,9 +58,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 +76,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..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,18 +64,29 @@ 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 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) @@ -96,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() @@ -130,30 +144,75 @@ 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) 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_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) + 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") diff --git a/src/FBO.cc b/src/FBO.cc index c575e7a..e2dad89 100644 --- a/src/FBO.cc +++ b/src/FBO.cc @@ -54,7 +54,7 @@ bool checkFBOStatus() { } // namespace std::unique_ptr createFBO(int width, int height) { - if (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); @@ -64,6 +64,8 @@ std::unique_ptr createFBO(int width, int height) { } } + + 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..74153f2 100644 --- a/src/OffscreenContextWGL.cc +++ b/src/OffscreenContextWGL.cc @@ -53,6 +53,7 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t .cbSize = sizeof(WNDCLASSEX), .style = CS_OWNDC, .lpfnWndProc = &DefWindowProc, + .hInstance = GetModuleHandle(nullptr), .lpszClassName = "OffscreenClass" }; // FIXME: Check for ERROR_CLASS_ALREADY_EXISTS ? @@ -61,7 +62,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 = { .nSize = sizeof(PIXELFORMATDESCRIPTOR), @@ -73,9 +82,16 @@ std::shared_ptr CreateOffscreenContextWGL(size_t width, size_t .cDepthBits = 24, .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) { @@ -87,21 +103,37 @@ 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; } + + diff --git a/src/main.cc b/src/main.cc index e879181..a01fa36 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 @@ -79,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; }); @@ -170,10 +184,12 @@ int main(int argc, char *argv[]) .majorGLVersion = requestMajor, .minorGLVersion = requestMinor, .gles = requestGLES, - .compatibilityProfile = argProfile == "compatibility", + .compatibilityProfile = (argProfile == "compatibility"), .gpu = argGPU, .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.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 e6771b2..cf8b319 100644 --- a/src/system-gl.h +++ b/src/system-gl.h @@ -35,15 +35,21 @@ 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(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 +