diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 59fb6a45..c2541d67 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -135,7 +135,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ env.MFEM_TOP_DIR }} - key: ${{ runner.os }}-build-${{ env.MFEM_TOP_DIR }}-${{ env.MFEM_COMMIT }}-${{ matrix.target }}-${{ matrix.build-system}}-v2.4 + key: ${{ runner.os }}-build-${{ env.MFEM_TOP_DIR }}-${{ env.MFEM_COMMIT }}-${{ matrix.build-system }}-${{ matrix.target }}-${{ matrix.mpi }}-v2.4 # We are using the defaults of the MFEM action here, which is to use master # branch. There is an implicit assumption here that mfem master hasn't @@ -199,7 +199,11 @@ jobs: run: | glvis_target="opt" [[ ${{ matrix.target }} == "dbg" ]] && glvis_target="debug"; - cd glvis && make ${glvis_target} -j3 + use_egl="NO" + [[ ${{ matrix.os }} == "ubuntu-latest" ]] && use_egl="YES" + use_cgl="NO" + [[ ${{ matrix.os }} == "macos-latest" ]] && use_cgl="YES" + cd glvis && make ${glvis_target} -j3 GLVIS_USE_EGL=${use_egl} GLVIS_USE_CGL=${use_cgl} - name: build GLVis (cmake) if: matrix.build-system == 'cmake' @@ -210,11 +214,17 @@ jobs: [[ ${{ matrix.target }} == "dbg" ]] && build_type="Debug"; [[ ${{ matrix.os }} == "windows-latest" ]] \ && toolchain_file="${VCPKG_INSTALLATION_ROOT}\\scripts\\buildsystems\\vcpkg.cmake" + use_egl="OFF" + [[ ${{ matrix.os }} == "ubuntu-latest" ]] && use_egl="ON" + use_cgl="NO" + [[ ${{ matrix.os }} == "macos-latest" ]] && use_cgl="YES" cd glvis && mkdir build && cd build cmake \ -D CMAKE_TOOLCHAIN_FILE:STRING=${toolchain_file} \ -D CMAKE_BUILD_TYPE:STRING=${build_type} \ -D ENABLE_TESTS:BOOL=TRUE \ + -D GLVIS_USE_EGL:BOOL=${use_egl} \ + -D GLVIS_USE_CGL:BOOL=${use_cgl} \ -D mfem_DIR:PATH=${GITHUB_WORKSPACE}/${MFEM_TOP_DIR}/build \ -D GLVIS_BASELINE_SYS=${{ matrix.os }} \ .. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f97a1252..a324d622 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,7 +118,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ env.MFEM_TOP_DIR }} - key: ${{ runner.os }}-build-${{ env.MFEM_TOP_DIR }}-${{ env.MFEM_COMMIT }}-${{ matrix.target }}-${{ matrix.build-system}}-v2.2 + key: ${{ runner.os }}-build-${{ env.MFEM_TOP_DIR }}-${{ env.MFEM_COMMIT }}-${{ matrix.build-system }}-${{ matrix.target }}-${{ matrix.mpi }}-v2.2 # We are using the defaults of the MFEM action here, which is to use master # branch. There is an implicit assumption here that mfem master hasn't diff --git a/CHANGELOG b/CHANGELOG index b70e3c7a..cddf0869 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,15 @@ Version 4.4.1 (development) Unlike previous GLVis releases, this version requires a C++17 compiler. +- Added headless (no GUI) visualization, relying on the EGL (Linux) or CGL + (MacOS) interface. It is available through '-hl' command-line option or + 'headless' script command (before the visualization commands). The end of the + loaded stream or script file ends the visualization in this mode. Note GLVis + must be compiled with with EGL/CGL (see INSTALL). + +- Added non-persistent mode of the server, when the server terminates after all + visualization windows are closed. + - Implemented DOF numbering in 2D scalar mode, pressing the `n` or `N` key cycles through: None → Elements → Edges → Vertices → DOFs. Parallel numbering is now by default 'local' to each rank; 'global' vs. 'local' numbering can be toggled with diff --git a/CMakeLists.txt b/CMakeLists.txt index 1eaa3fcd..b3a41a63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,14 @@ option(GLVIS_USE_LIBPNG "Use libpng for taking screenshots internally" ON) +option(GLVIS_USE_EGL + "Use EGL for headless rendering" + OFF) + +option(GLVIS_USE_CGL + "Use CGL for headless rendering" + APPLE) + # # Handle a few other definitions # @@ -237,6 +245,23 @@ if (NOT EMSCRIPTEN) message(FATAL_ERROR "Fontconfig not found.") endif (FONTCONFIG_LIBRARY) + # Find EGL + if (GLVIS_USE_EGL) + find_package(OpenGL OPTIONAL_COMPONENTS EGL) + if (OpenGL_EGL_FOUND) + list(APPEND _glvis_compile_defs "GLVIS_USE_EGL") + list(APPEND _glvis_libraries OpenGL::EGL) + else() + message(WARNING "EGL library not found. EGL disabled.") + set(GLVIS_USE_EGL OFF) + endif (OpenGL_EGL_FOUND) + endif (GLVIS_USE_EGL) + + # Find CGL + if (GLVIS_USE_CGL) + list(APPEND _glvis_compile_defs "GLVIS_USE_CGL") + endif (GLVIS_USE_CGL) + # Find threading library find_package(Threads REQUIRED) list(APPEND _glvis_libraries "${CMAKE_THREAD_LIBS_INIT}") diff --git a/INSTALL b/INSTALL index 39216342..10b6198c 100644 --- a/INSTALL +++ b/INSTALL @@ -38,6 +38,9 @@ Besides a C++ compiler, GLVis depends on the following external packages: - the XQuartz app (on Mac OS X, if running GLVis remotely through ssh) https://www.xquartz.org +- the EGL library; used for headless rendering (optional) + https://www.khronos.org/egl + There are two build systems, one based on GNU make and one based on CMake, as described below. Choose the one that matches the build system you used to build MFEM. @@ -87,6 +90,7 @@ cmake \ \ -D GLVIS_USE_LIBTIFF=OFF \ -D GLVIS_USE_LIBPNG=ON \ + -D GLVIS_USE_EGL=OFF \ \ -D MFEM_DIR=/path/to/directory/with/MFEMConfig.cmake \ \ @@ -111,6 +115,11 @@ Some important variables for CMake are: - GLVIS_USE_LIBTIFF: Use libtiff for creating screenshots. Default is "OFF". +- GLVIS_USE_EGL: Use EGL for headless rendering. Default is "OFF". + +- GLVIS_USE_CGL: Use CGL for headless rendering on Mac OS X. Default is "ON" on + this platform, "OFF" otherwise. + - GLVIS_MULTISAMPLE and GLVIS_MS_LINEWIDTH: See building considerations below for more information on these variables. diff --git a/glvis.cpp b/glvis.cpp index e9be62aa..683d7b8a 100644 --- a/glvis.cpp +++ b/glvis.cpp @@ -42,6 +42,8 @@ #include "lib/stream_reader.hpp" #include "lib/file_reader.hpp" #include "lib/coll_reader.hpp" +#include "lib/sdl/sdl.hpp" +#include "lib/egl/egl.hpp" using namespace std; using namespace mfem; @@ -77,11 +79,13 @@ class Session public: Session(bool fix_elem_orient, bool save_coloring, - string plot_caption) + string plot_caption, + bool headless) { win.data_state.fix_elem_orient = fix_elem_orient; win.data_state.save_coloring = save_coloring; win.plot_caption = plot_caption; + win.headless = headless; } Session(Window other_win) @@ -154,7 +158,7 @@ class Session }; void GLVisServer(int portnum, bool save_stream, bool fix_elem_orient, - bool save_coloring, string plot_caption) + bool save_coloring, string plot_caption, bool headless = false) { std::vector current_sessions; string data_type; @@ -304,7 +308,7 @@ void GLVisServer(int portnum, bool save_stream, bool fix_elem_orient, while (1); } - Session new_session(fix_elem_orient, save_coloring, plot_caption); + Session new_session(fix_elem_orient, save_coloring, plot_caption, headless); constexpr int tmp_filename_size = 50; char tmp_file[tmp_filename_size]; @@ -381,6 +385,7 @@ int main (int argc, char *argv[]) const char *window_title = string_default; const char *font_name = string_default; int portnum = 19916; + bool persistent = true; int multisample = GetMultisample(); double line_width = GetLineWidth(); double ms_line_width = GetLineWidthMS(); @@ -451,6 +456,9 @@ int main (int argc, char *argv[]) "Save the mesh coloring generated when opening only a mesh."); args.AddOption(&portnum, "-p", "--listen-port", "Specify the port number on which to accept connections."); + args.AddOption(&persistent, "-pr", "--persistent", + "-no-pr", "--no-persistent", + "Keep server running after all windows are closed."); args.AddOption(&secure, "-sec", "--secure-sockets", "-no-sec", "--standard-sockets", "Enable or disable GnuTLS secure sockets."); @@ -466,6 +474,9 @@ int main (int argc, char *argv[]) "Set the window height."); args.AddOption(&window_title, "-wt", "--window-title", "Set the window title."); + args.AddOption(&win.headless, "-hl", "--headless", + "-no-hl", "--no-headless", + "Start headless (no GUI) visualization."); args.AddOption(&c_plot_caption, "-c", "--plot-caption", "Set the plot caption (visible when colorbar is visible)."); args.AddOption(&font_name, "-fn", "--font", @@ -607,20 +618,21 @@ int main (int argc, char *argv[]) // check for saved stream file if (stream_file != string_none) { + // backup the headless flag as the window is moved + const bool headless = win.headless; + // Make sure the singleton object returned by GetMainThread() is // initialized from the main thread. - GetMainThread(); + GetMainThread(headless); - Session stream_session(win.data_state.fix_elem_orient, - win.data_state.save_coloring, - win.plot_caption); + Session stream_session(std::move(win)); if (!stream_session.StartSavedSession(stream_file)) { return 1; } - SDLMainLoop(); + MainThreadLoop(headless); return 0; } @@ -676,17 +688,16 @@ int main (int argc, char *argv[]) { // Make sure the singleton object returned by GetMainThread() is // initialized from the main thread. - GetMainThread(); + GetMainThread(win.headless); // Run server in new thread std::thread serverThread{GLVisServer, portnum, save_stream, win.data_state.fix_elem_orient, win.data_state.save_coloring, - win.plot_caption}; - - // Start SDL in main thread - SDLMainLoop(true); + win.plot_caption, win.headless}; + // Start message loop in main thread + MainThreadLoop(win.headless, persistent); serverThread.detach(); } else // input != 1, non-server mode @@ -792,14 +803,17 @@ int main (int argc, char *argv[]) if (ierr) { exit(ierr); } } + // backup the headless flag as the window is moved + const bool headless = win.headless; + // Make sure the singleton object returned by GetMainThread() is // initialized from the main thread. - GetMainThread(); + GetMainThread(headless); Session single_session(std::move(win)); single_session.StartSession(); - SDLMainLoop(); + MainThreadLoop(headless); } cout << "Thank you for using GLVis." << endl; diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index e3e5bc86..399588bb 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -10,25 +10,28 @@ # CONTRIBUTING.md for details. list(APPEND SOURCES + egl/egl.cpp + egl/egl_main.cpp gl/renderer.cpp gl/renderer_core.cpp gl/shader.cpp gl/types.cpp + sdl/sdl.cpp + sdl/sdl_helper.cpp + sdl/sdl_main.cpp aux_vis.cpp coll_reader.cpp data_state.cpp file_reader.cpp font.cpp gltf.cpp + glwindow.cpp material.cpp openglvis.cpp palettes.cpp palettes_default.cpp palettes_base.cpp script_controller.cpp - sdl.cpp - sdl_helper.cpp - sdl_main.cpp stream_reader.cpp vsdata.cpp vssolution.cpp @@ -38,12 +41,18 @@ list(APPEND SOURCES window.cpp) list(APPEND HEADERS + egl/egl.hpp + egl/egl_main.hpp gl/attr_traits.hpp gl/platform_gl.hpp gl/renderer.hpp gl/renderer_core.hpp gl/shader.hpp gl/types.hpp + sdl/sdl.hpp + sdl/sdl_helper.hpp + sdl/sdl_main.hpp + sdl/sdl_mac.hpp aux_vis.hpp coll_reader.hpp data_state.hpp @@ -51,16 +60,13 @@ list(APPEND HEADERS font.hpp geom_utils.hpp gltf.hpp + glwindow.hpp logo.hpp material.hpp openglvis.hpp palettes.hpp palettes_base.hpp script_controller.hpp - sdl.hpp - sdl_helper.hpp - sdl_main.hpp - sdl_mac.hpp stream_reader.hpp visual.hpp vsdata.hpp @@ -89,15 +95,15 @@ if(EMSCRIPTEN) target_link_libraries(libglvis PUBLIC "${_glvis_libraries}") else() # Desktop build target - list(APPEND SOURCES gl/renderer_ff.cpp threads.cpp gl2ps.c sdl_x11.cpp) - list(APPEND HEADERS gl/renderer_ff.hpp threads.hpp gl2ps.h sdl_x11.hpp) + list(APPEND SOURCES gl/renderer_ff.cpp threads.cpp gl2ps.c sdl/sdl_x11.cpp) + list(APPEND HEADERS gl/renderer_ff.hpp threads.hpp gl2ps.h sdl/sdl_x11.hpp) if (APPLE) - list(APPEND SOURCES sdl_mac.mm) - list(APPEND HEADERS sdl_mac.hpp) + list(APPEND SOURCES sdl/sdl_mac.mm) + list(APPEND HEADERS sdl/sdl_mac.hpp) endif() if (WIN32) - list(APPEND SOURCES sdl_windows.cpp) - list(APPEND HEADERS sdl_windows.hpp) + list(APPEND SOURCES sdl/sdl_windows.cpp) + list(APPEND HEADERS sdl/sdl_windows.hpp) endif() add_library(glvis ${SOURCES} ${HEADERS}) target_compile_definitions(glvis PUBLIC "${_glvis_compile_defs}") diff --git a/lib/aux_js.cpp b/lib/aux_js.cpp index 401af562..3fcfbd4b 100644 --- a/lib/aux_js.cpp +++ b/lib/aux_js.cpp @@ -13,6 +13,7 @@ #include "palettes.hpp" #include "stream_reader.hpp" #include "visual.hpp" +#include "glwindow.hpp" #ifdef GLVIS_USE_LIBPNG #include @@ -44,16 +45,19 @@ using namespace mfem; void display(std::stringstream & commands, const int w, const int h) { // reset antialiasing - win.wnd->getRenderer().setAntialiasing(0); + if (win.wnd) + { + win.wnd->getRenderer().setAntialiasing(0); + } - std::string word; + std::string word, keys; double minv = 0.0, maxv = 0.0; while (commands >> word) { if (word == "keys") { std::cout << "parsing 'keys'" << std::endl; - commands >> win.data_state.keys; + commands >> keys; } else if (word == "valuerange") { @@ -72,9 +76,9 @@ void display(std::stringstream & commands, const int w, const int h) win.window_w = w; win.window_h = h; - win.GLVisInitVis({}); + if (!win.GLVisInitVis({})) { return; } - CallKeySequence(win.data_state.keys.c_str()); + CallKeySequence(keys.c_str()); if (minv || maxv) { @@ -244,7 +248,7 @@ void processKey(int sym, bool ctrl=false, bool shift=false, bool alt=false) mod |= ctrl ? KMOD_CTRL : 0; mod |= shift ? KMOD_SHIFT : 0; mod |= alt ? KMOD_ALT : 0; - win.wnd->callKeyDown(sym, mod); + win.wnd->callKeyDown(sym, (SDL_Keymod)mod); } void setupResizeEventCallback(const std::string & id) diff --git a/lib/aux_vis.cpp b/lib/aux_vis.cpp index c76278a8..9be53342 100644 --- a/lib/aux_vis.cpp +++ b/lib/aux_vis.cpp @@ -16,9 +16,12 @@ #include #include +#include "sdl/sdl.hpp" +#include "sdl/sdl_main.hpp" +#include "egl/egl.hpp" +#include "egl/egl_main.hpp" #include "gl/types.hpp" #include "palettes.hpp" -#include "sdl.hpp" #include "threads.hpp" #ifndef __EMSCRIPTEN__ #include "gl2ps.h" @@ -53,13 +56,25 @@ static int glvis_multisample = -1; static float line_w = 1.f; static float line_w_aa = gl3::LINE_WIDTH_AA; -static thread_local SdlWindow * wnd = nullptr; +static thread_local GLWindow *wnd = nullptr; +static thread_local SdlWindow *sdl_wnd = nullptr; static bool wndLegacyGl = false; -bool wndUseHiDPI = true; // shared with sdl_main.cpp +static bool wndUseHiDPI = true; -void SDLMainLoop(bool server_mode) +MainThread& GetMainThread(bool headless) { - SdlWindow::StartSDL(server_mode); +#if defined(GLVIS_USE_EGL) or defined(GLVIS_USE_CGL) + if (headless) + { + return EglMainThread::Get(); + } +#endif + return GetSdlMainThread(); +} + +void MainThreadLoop(bool headless, bool server_mode) +{ + GetMainThread(headless).MainLoop(server_mode); } void SetGLVisCommand(GLVisCommand *cmd) @@ -67,7 +82,12 @@ void SetGLVisCommand(GLVisCommand *cmd) glvis_command = cmd; } -SdlWindow * GetAppWindow() +SdlWindow * GetSdlWindow() +{ + return sdl_wnd; +} + +GLWindow *GetAppWindow() { return wnd; } @@ -87,28 +107,70 @@ void SetUseHiDPI(bool status) wndUseHiDPI = status; } +bool GetUseHiDPI() +{ + return wndUseHiDPI; +} + void MyExpose(GLsizei w, GLsizei h); void MyExpose(); -SdlWindow* InitVisualization(const char name[], int x, int y, int w, int h) +GLWindow* InitVisualization(const char name[], int x, int y, int w, int h, + bool headless) { - #ifdef GLVIS_DEBUG - cout << "OpenGL Visualization" << endl; + if (!headless) { cout << "OpenGL Visualization" << endl; } + else + { +#if defined(GLVIS_USE_EGL) + cout << "OpenGL+EGL Visualization" << endl; +#elif defined(GLVIS_USE_CGL) + cout << "OpenGL+CGL Visualization" << endl; +#else + cout << "Headless rendering requires EGL or CGL!" << endl; +#endif + } #endif - if (!wnd) + + if (!headless) { - wnd = new SdlWindow(); - if (!wnd->createWindow(name, x, y, w, h, wndLegacyGl)) + if (!sdl_wnd) { - delete wnd; - wnd = nullptr; - return NULL; + wnd = sdl_wnd = new SdlWindow(); + if (!sdl_wnd->createWindow(name, x, y, w, h, wndLegacyGl)) + { + delete wnd; + wnd = sdl_wnd = nullptr; + return NULL; + } + } + else + { + sdl_wnd->clearEvents(); } } else { - wnd->clearEvents(); +#if defined(GLVIS_USE_EGL) or defined(GLVIS_USE_CGL) + sdl_wnd = nullptr; + if (!wnd) + { + wnd = new EglWindow(); + if (!wnd->createWindow(name, x, y, w, h, wndLegacyGl)) + { + delete wnd; + wnd = nullptr; + return NULL; + } + } + else + { + wnd->clearEvents(); + } +#else // GLVIS_USE_EGL || GLVIS_USE_CGL + cerr << "EGL or CGL are required for headless rendering!" << endl; + return NULL; +#endif // GLVIS_USE_EGL || GLVIS_USE_CGL } #ifdef GLVIS_DEBUG @@ -133,7 +195,10 @@ SdlWindow* InitVisualization(const char name[], int x, int y, int w, int h) wnd->setOnMouseUp(SDL_BUTTON_RIGHT, RightButtonUp); wnd->setOnMouseMove(SDL_BUTTON_RIGHT, RightButtonLoc); - wnd->setTouchPinchCallback(TouchPinch); + if (sdl_wnd) + { + sdl_wnd->setTouchPinchCallback(TouchPinch); + } // auxKeyFunc (AUX_p, KeyCtrlP); // handled in vsdata.cpp wnd->setOnKeyDown (SDLK_s, KeyS); @@ -198,11 +263,6 @@ SdlWindow* InitVisualization(const char name[], int x, int y, int w, int h) #ifndef __EMSCRIPTEN__ wnd->setOnKeyDown(SDLK_LEFTPAREN, ShrinkWindow); wnd->setOnKeyDown(SDLK_RIGHTPAREN, EnlargeWindow); - - if (locscene) - { - delete locscene; - } #endif locscene = nullptr; @@ -377,7 +437,7 @@ void RunVisualization() #endif InitIdleFuncs(); visualize = 0; - wnd = nullptr; + wnd = sdl_wnd = nullptr; } void SendExposeEvent() @@ -606,7 +666,7 @@ inline void ComputeSphereAngles(int &newx, int &newy, new_sph_t = atan2(y, x); } -void LeftButtonDown (EventInfo *event) +void LeftButtonDown(GLWindow::MouseEventInfo *event) { locscene -> spinning = 0; RemoveIdleFunc(MainLoop); @@ -624,7 +684,7 @@ void LeftButtonDown (EventInfo *event) starty = oldy; } -void LeftButtonLoc (EventInfo *event) +void LeftButtonLoc(GLWindow::MouseEventInfo *event) { GLint newx = event->mouse_x; GLint newy = event->mouse_y; @@ -682,7 +742,7 @@ void LeftButtonLoc (EventInfo *event) } } -void LeftButtonUp (EventInfo *event) +void LeftButtonUp(GLWindow::MouseEventInfo *event) { GLint newx = event->mouse_x; GLint newy = event->mouse_y; @@ -708,13 +768,13 @@ void LeftButtonUp (EventInfo *event) } } -void MiddleButtonDown (EventInfo *event) +void MiddleButtonDown(GLWindow::MouseEventInfo *event) { startx = oldx = event->mouse_x; starty = oldy = event->mouse_y; } -void MiddleButtonLoc (EventInfo *event) +void MiddleButtonLoc(GLWindow::MouseEventInfo *event) { GLint newx = event->mouse_x; GLint newy = event->mouse_y; @@ -782,16 +842,16 @@ void MiddleButtonLoc (EventInfo *event) oldy = newy; } -void MiddleButtonUp (EventInfo*) +void MiddleButtonUp(GLWindow::MouseEventInfo*) {} -void RightButtonDown (EventInfo *event) +void RightButtonDown(GLWindow::MouseEventInfo *event) { startx = oldx = event->mouse_x; starty = oldy = event->mouse_y; } -void RightButtonLoc (EventInfo *event) +void RightButtonLoc(GLWindow::MouseEventInfo *event) { GLint newx = event->mouse_x; GLint newy = event->mouse_y; @@ -839,7 +899,7 @@ void RightButtonLoc (EventInfo *event) oldy = newy; } -void RightButtonUp (EventInfo*) +void RightButtonUp(GLWindow::MouseEventInfo*) {} void TouchPinch(SDL_MultiGestureEvent & e) @@ -972,7 +1032,7 @@ int SaveAsPNG(const char *fname, int w, int h, bool is_hidpi, bool with_alpha, } png_uint_32 ppi = is_hidpi ? 144 : 72; // pixels/inch - png_uint_32 ppm = ppi/0.0254 + 0.5; // pixels/meter + auto ppm = static_cast(ppi/0.0254 + 0.5); // pixels/meter png_set_pHYs(png_ptr, info_ptr, ppm, ppm, PNG_RESOLUTION_METER); png_init_io(png_ptr, fp); diff --git a/lib/aux_vis.hpp b/lib/aux_vis.hpp index c29c255f..f64e1734 100644 --- a/lib/aux_vis.hpp +++ b/lib/aux_vis.hpp @@ -15,18 +15,20 @@ #include "gl/types.hpp" #include "openglvis.hpp" -#include "sdl.hpp" +#include "glwindow.hpp" #include "font.hpp" #include -void SDLMainLoop(bool server_mode = false); +MainThread& GetMainThread(bool headless = false); +void MainThreadLoop(bool headless = false, bool server_mode = false); class GLVisCommand; void SetGLVisCommand(GLVisCommand *cmd); /// Initializes the visualization and some keys. -SdlWindow* InitVisualization(const char name[], int x, int y, int w, int h); +GLWindow* InitVisualization(const char name[], int x, int y, int w, int h, + bool headless = false); void SetVisualizationScene(VisualizationScene * scene, int view = 3, const char *keys = NULL); @@ -41,7 +43,9 @@ void MyExpose(); void MainLoop(); -SdlWindow * GetAppWindow(); +class SdlWindow; +SdlWindow* GetSdlWindow(); +GLWindow* GetAppWindow(); VisualizationScene * GetVisualizationScene(); void SetLegacyGLOnly(bool status); @@ -49,15 +53,15 @@ void SetLegacyGLOnly(bool status); void AddIdleFunc(void (*Func)(void)); void RemoveIdleFunc(void (*Func)(void)); -void LeftButtonDown (EventInfo *event); -void LeftButtonLoc (EventInfo *event); -void LeftButtonUp (EventInfo *event); -void MiddleButtonDown(EventInfo *event); -void MiddleButtonLoc (EventInfo *event); -void MiddleButtonUp (EventInfo *event); -void RightButtonDown (EventInfo *event); -void RightButtonLoc (EventInfo *event); -void RightButtonUp (EventInfo *event); +void LeftButtonDown (GLWindow::MouseEventInfo *event); +void LeftButtonLoc (GLWindow::MouseEventInfo *event); +void LeftButtonUp (GLWindow::MouseEventInfo *event); +void MiddleButtonDown(GLWindow::MouseEventInfo *event); +void MiddleButtonLoc (GLWindow::MouseEventInfo *event); +void MiddleButtonUp (GLWindow::MouseEventInfo *event); +void RightButtonDown (GLWindow::MouseEventInfo *event); +void RightButtonLoc (GLWindow::MouseEventInfo *event); +void RightButtonUp (GLWindow::MouseEventInfo *event); void TouchPinch(SDL_MultiGestureEvent & e); @@ -137,6 +141,7 @@ bool SetFont(const std::vector& patterns, int height); void SetFont(const std::string& fn); void SetUseHiDPI(bool status); +bool GetUseHiDPI(); std::function NumberFormatter(int precision=4, char format='d', bool showsign=false); diff --git a/lib/egl/egl.cpp b/lib/egl/egl.cpp new file mode 100644 index 00000000..79e15fd7 --- /dev/null +++ b/lib/egl/egl.cpp @@ -0,0 +1,219 @@ +// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced +// at the Lawrence Livermore National Laboratory. All Rights reserved. See files +// LICENSE and NOTICE for details. LLNL-CODE-443271. +// +// This file is part of the GLVis visualization tool and library. For more +// information and source code availability see https://glvis.org. +// +// GLVis is free software; you can redistribute it and/or modify it under the +// terms of the BSD-3 license. We welcome feedback and contributions, see file +// CONTRIBUTING.md for details. + +#if defined(GLVIS_USE_EGL) || defined(GLVIS_USE_CGL) + +#include +#include +#include + +#include "egl.hpp" +#include "egl_main.hpp" +#include "../aux_vis.hpp" + +#ifdef GLVIS_DEBUG +#define PRINT_DEBUG(s) std::cerr << s +#else +#define PRINT_DEBUG(s) {} +#endif + +using namespace std; + +EglWindow::~EglWindow() +{ + EglMainThread::Get().DeleteWindow(this, handle); +} + +bool EglWindow::createWindow(const char *, int, int, int w, int h, + bool legacyGlOnly) +{ + handle = EglMainThread::Get().CreateWindow(this, w, h, legacyGlOnly); + if (!handle.isInitialized()) + { + return false; + } + +#ifdef GLVIS_USE_CGL + return true; // CGL already called initGLEW() during CreateWindow() +#endif + +#ifndef __EMSCRIPTEN__ + glEnable(GL_DEBUG_OUTPUT); +#endif + + PRINT_DEBUG("EGL context is ready" << endl); + + return initGLEW(legacyGlOnly); +} + +void EglWindow::queueEvents(vector events) +{ + { + lock_guard evt_guard{event_mutex}; + waiting_events.insert(waiting_events.end(), events.begin(), events.end()); + } + if (is_multithreaded) + { + events_available.notify_all(); + } +} + +void EglWindow::mainLoop() +{ + running = true; + while (running) + { + mainIter(); + } +} + +void EglWindow::mainIter() +{ + bool sleep = false; + bool events_pending = false; + { + lock_guard evt_guard{event_mutex}; + events_pending = !waiting_events.empty(); + } + if (events_pending) + { + do + { + Event e; + // Fetch next event from the queue + { + lock_guard evt_guard{event_mutex}; + e = waiting_events.front(); + waiting_events.pop_front(); + events_pending = !waiting_events.empty(); + } + + switch (e.type) + { + case EventType::Keydown: + if (onKeyDown[e.event.keydown.k]) + { + onKeyDown[e.event.keydown.k](e.event.keydown.m); + recordKey(e.event.keydown.k, e.event.keydown.m); + } + break; + case EventType::Screenshot: + if (isExposePending()) + { + onExpose(); + wnd_state = RenderState::Updated; + } + Screenshot(screenshot_filename.c_str(), e.event.screenshot.convert); + break; + case EventType::Quit: + running = false; + break; + } + } + while (events_pending); + } + else if (onIdle) + { + sleep = onIdle(); + } + else + { + // No actions performed this iteration. + sleep = true; + } + + if (isExposePending()) + { + onExpose(); + wnd_state = RenderState::Updated; + } + else if (sleep) + { + unique_lock event_lock{event_mutex}; + events_available.wait(event_lock, [this]() + { + // Sleep until events from WM or glvis_command can be handled + return !waiting_events.empty() || call_idle_func; + }); + } +} + +void EglWindow::signalLoop() +{ + // Note: not executed from the main thread + { + lock_guard evt_guard{event_mutex}; + call_idle_func = true; + } + events_available.notify_all(); +} + +void EglWindow::getGLDrawSize(int& w, int& h) const +{ +#ifdef GLVIS_USE_EGL + EGLint egl_w, egl_h; + + EGLDisplay disp = EglMainThread::Get().GetDisplay(); + eglQuerySurface(disp, handle.surf, EGL_WIDTH, &egl_w); + eglQuerySurface(disp, handle.surf, EGL_HEIGHT, &egl_h); + w = egl_w; + h = egl_h; +#endif +#ifdef GLVIS_USE_CGL + GLint cgl_w, cgl_h; + // Bind the color renderbuffer + glBindRenderbuffer(GL_RENDERBUFFER, handle.buf_color); + assert(glGetError() == GL_NO_ERROR); + // Query width and height + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &cgl_w); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &cgl_h); + assert(glGetError() == GL_NO_ERROR); + // Unbind to restore state + glBindRenderbuffer(GL_RENDERBUFFER, 0); + assert(glGetError() == GL_NO_ERROR); + w = cgl_w; + h = cgl_h; +#endif +} + +bool EglWindow::isHighDpi() const +{ + return GetUseHiDPI(); +} + +void EglWindow::setWindowSize(int w, int h) +{ + EglMainThread::Get().ResizeWindow(handle, w, h); +} + +void EglWindow::signalKeyDown(SDL_Keycode k, SDL_Keymod m) +{ + Event::Events e; + e.keydown = {k, m}; + queueEvents({{EventType::Keydown, e}}); +} + +void EglWindow::signalQuit() +{ + queueEvents({{EventType::Quit, {}}}); +} + +void EglWindow::screenshot(string filename, bool convert) +{ + screenshot_filename = filename; + Event::Events e; + e.screenshot = {convert}; + queueEvents({{EventType::Screenshot, e}}); + // Queue up an expose, so Screenshot() can pull image from updated buffer + signalExpose(); +} + +#endif // GLVIS_USE_EGL || GLVIS_USE_CGL diff --git a/lib/egl/egl.hpp b/lib/egl/egl.hpp new file mode 100644 index 00000000..9e8763e7 --- /dev/null +++ b/lib/egl/egl.hpp @@ -0,0 +1,149 @@ +// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced +// at the Lawrence Livermore National Laboratory. All Rights reserved. See files +// LICENSE and NOTICE for details. LLNL-CODE-443271. +// +// This file is part of the GLVis visualization tool and library. For more +// information and source code availability see https://glvis.org. +// +// GLVis is free software; you can redistribute it and/or modify it under the +// terms of the BSD-3 license. We welcome feedback and contributions, see file +// CONTRIBUTING.md for details. + +#ifndef GLVIS_EGL_HPP +#define GLVIS_EGL_HPP + +#if defined(GLVIS_USE_EGL) || defined(GLVIS_USE_CGL) + +#include "../glwindow.hpp" + +#ifdef GLVIS_USE_EGL +#include +#endif + +#ifdef GLVIS_USE_CGL +#define GL_SILENCE_DEPRECATION // CGL has been deprecated since MacOS 10.14 +#include +#endif + +#include +#include +#include +#include + +class EglWindow : public GLWindow +{ +public: + struct Handle + { +#ifdef GLVIS_USE_EGL + EGLSurface surf {EGL_NO_SURFACE}; + EGLContext ctx{EGL_NO_CONTEXT}; + EGLConfig eglCfg{}; +#endif +#ifdef GLVIS_USE_CGL + GLuint buf_frame {}, buf_color {}, buf_depth {}; + CGLPixelFormatObj pix {}; + + using CGLContextDeleter = void(*)(CGLContextObj); + std::unique_ptr<_CGLContextObject, CGLContextDeleter> ctx + {nullptr, [](CGLContextObj ctx) { CGLDestroyContext(ctx); }}; +#endif + + bool isInitialized() + { +#ifdef GLVIS_USE_EGL + return surf != EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT; +#endif +#ifdef GLVIS_USE_CGL + return ctx != nullptr; +#endif + } + }; + +private: + Handle handle; + + bool running {false}; + bool is_multithreaded {true}; + bool call_idle_func {false}; + + enum class EventType + { + Keydown, + Screenshot, + Quit, + }; + + struct Event + { + EventType type; + union Events + { + struct Keydown + { + SDL_Keycode k; + SDL_Keymod m; + } keydown; + + struct Screenshot + { + bool convert; + } screenshot; + + struct Quit { } quit; + } event; + }; + + std::string screenshot_filename; + std::condition_variable events_available; + std::mutex event_mutex; + std::deque waiting_events; + + void queueEvents(std::vector events); + +public: + EglWindow() = default; + ~EglWindow(); + + /** @brief Creates a new OpenGL window. Returns false if EGL or OpenGL + initialization fails. */ + bool createWindow(const char *title, int x, int y, int w, int h, + bool legacyGlOnly) override; + + /// Runs the window loop. + void mainLoop() override; + void mainIter() override; + + void signalLoop() override; + + void getWindowSize(int& w, int& h) const override { getGLDrawSize(w, h); } + void getGLDrawSize(int& w, int& h) const override; + + // use the default values of SdlWindow, LoadFont() ignores them anyway + void getDpi(int& wdpi, int& hdpi) const override { wdpi = hdpi = 72; } + bool isHighDpi() const override; + + void setWindowSize(int w, int h) override; + +#if defined(GLVIS_USE_EGL) + bool isWindowInitialized() const override { return handle.surf != EGL_NO_SURFACE; } + bool isGlInitialized() const override { return handle.ctx != EGL_NO_CONTEXT; } +#elif defined(GLVIS_USE_CGL) + bool isWindowInitialized() const override { return isGlInitialized(); } + bool isGlInitialized() const override { return handle.ctx != nullptr; } +#endif + + void signalKeyDown(SDL_Keycode k, SDL_Keymod m = KMOD_NONE) override; + void signalQuit() override; + // as there is no swap, switch to updated state right away + void signalSwap() override { wnd_state = RenderState::Updated; } + + // used in Screenshot, as there is no swapping, the single buffer is always + // up to date and can be read directly + bool isSwapPending() const override { return true; } + + void screenshot(std::string filename, bool convert = false) override; +}; + +#endif // GLVIS_USE_EGL || GLVIS_USE_CGL +#endif // GLVIS_EGL_HPP diff --git a/lib/egl/egl_main.cpp b/lib/egl/egl_main.cpp new file mode 100644 index 00000000..073842d6 --- /dev/null +++ b/lib/egl/egl_main.cpp @@ -0,0 +1,604 @@ +// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced +// at the Lawrence Livermore National Laboratory. All Rights reserved. See files +// LICENSE and NOTICE for details. LLNL-CODE-443271. +// +// This file is part of the GLVis visualization tool and library. For more +// information and source code availability see https://glvis.org. +// +// GLVis is free software; you can redistribute it and/or modify it under the +// terms of the BSD-3 license. We welcome feedback and contributions, see file +// CONTRIBUTING.md for details. + +#if defined(GLVIS_USE_EGL) || defined(GLVIS_USE_CGL) + +#include +#include + +#include "egl_main.hpp" +#include "../aux_vis.hpp" + +#ifdef GLVIS_DEBUG +#define PRINT_DEBUG(s) std::cerr << s +#else +#define PRINT_DEBUG(s) {} +#endif + +using namespace std; + +struct EglMainThread::CreateWndCmd +{ + EglWindow *wnd; + int w, h; + bool legacy_gl; + promise out_handle; +}; + +struct EglMainThread::ResizeWndCmd +{ + Handle *handle; + int w, h; +}; + +struct EglMainThread::DeleteWndCmd +{ + EglWindow *wnd; + Handle *handle; +}; + +bool EglMainThread::CreateWndImpl(CreateWndCmd &cmd) +{ + const int multisamples = GetMultisample(); + Handle new_handle; + +#ifdef GLVIS_USE_EGL + // 1. Select an appropriate configuration + EGLint configAttribs[] = + { + EGL_SAMPLE_BUFFERS, (multisamples > 0)?(1):(0), // must be first + EGL_SAMPLES, multisamples, + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_CONFORMANT, EGL_OPENGL_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_NONE + }; + + EGLint numConfigs; + + if (multisamples > 0) + { + if (!eglChooseConfig(disp, configAttribs, NULL, 0, &numConfigs) || + numConfigs < 1) + { + std::cerr << "EGL with multisampling is not supported, turning it off" << + std::endl; + // turn off multisampling + configAttribs[1] = 0; + } + } + + if (!eglChooseConfig(disp, configAttribs, &new_handle.eglCfg, 1, &numConfigs) || + numConfigs < 1) + { + std::cerr << "Cannot find working EGL configuration!" << std::endl; + return false; + } + + // 2. Create a surface + const EGLint pbufferAttribs[] = + { + EGL_WIDTH, cmd.w, + EGL_HEIGHT, cmd.h, + EGL_NONE + }; + + new_handle.surf = eglCreatePbufferSurface(disp, new_handle.eglCfg, + pbufferAttribs); + if (new_handle.surf == EGL_NO_SURFACE) + { + std::cerr << "Cannot create a pixel buffer, error: " << eglGetError() << + std::endl; + return false; + } +#endif +#ifdef GLVIS_USE_CGL + vector pixAttrs = + { + kCGLPFASampleBuffers, CGLPixelFormatAttribute((multisamples > 0)?(1):(0)), // must be first + kCGLPFASamples, CGLPixelFormatAttribute(multisamples), + kCGLPFAColorSize, CGLPixelFormatAttribute(24), + kCGLPFAAlphaSize, CGLPixelFormatAttribute(8), + kCGLPFADepthSize, CGLPixelFormatAttribute(24), + kCGLPFAAccelerated, // must be last + CGLPixelFormatAttribute(0) + }; + + if (cmd.legacy_gl) + { + // insert legacy OpenGL compatibility requirement + auto it = (pixAttrs.end() -= 2); + pixAttrs.insert(it, {kCGLPFAOpenGLProfile, CGLPixelFormatAttribute(kCGLOGLPVersion_Legacy)}); + } + + GLint numConfigs; + CGLError err = CGLChoosePixelFormat(pixAttrs.data(), &new_handle.pix, + &numConfigs); + if (multisamples > 0 && (err != kCGLNoError || numConfigs < 1)) + { + std::cerr << "CGL with multisampling is not supported, turning it off" << + std::endl; + pixAttrs[1] = CGLPixelFormatAttribute(0); + err = CGLChoosePixelFormat(pixAttrs.data(), &new_handle.pix, &numConfigs); + } + if (err != kCGLNoError || numConfigs < 1) + { + std::cerr << "CGL with hardware acceleration not supported, turning it off" << + std::endl; + pixAttrs.pop_back(); + pixAttrs.back() = CGLPixelFormatAttribute(0); + err = CGLChoosePixelFormat(pixAttrs.data(), &new_handle.pix, &numConfigs); + } + if (err != kCGLNoError || numConfigs < 1) + { + std::cerr << "Cannot set up CGL pixel format configuration, error: " + << CGLErrorString(err) << std::endl; + return false; + } + + CGLContextObj ctx; + err = CGLCreateContext(new_handle.pix, nullptr, &ctx); + if (err != kCGLNoError) + { + std::cerr << "Cannot create an OpenGL context, error: " + << CGLErrorString(err) << std::endl; + return false; + } + new_handle.ctx.reset(ctx); +#endif // GLVIS_USE_CGL + + windows.push_back(cmd.wnd); + if (num_windows < 0) + { + num_windows = 1; + } + else + { + num_windows++; + } + cmd.out_handle.set_value(std::move(new_handle)); + + return true; +} + +bool EglMainThread::ResizeWndImpl(ResizeWndCmd &cmd) +{ +#ifdef GLVIS_USE_EGL + const EGLint pbufferAttribs[] = + { + EGL_WIDTH, cmd.w, + EGL_HEIGHT, cmd.h, + EGL_NONE + }; + + EGLSurface surf_new = eglCreatePbufferSurface(disp, cmd.handle->eglCfg, + pbufferAttribs); + if (surf_new == EGL_NO_SURFACE) + { + std::cerr << "Cannot create a pixel buffer, error: " << eglGetError() << + std::endl; + return false; + } + + if (!eglDestroySurface(disp, cmd.handle->surf)) + { + std::cerr << "Cannot destroy surface, error: " << eglGetError() << std::endl; + return false; + } + + cmd.handle->surf = surf_new; +#endif + return true; +} + +bool EglMainThread::DeleteWndImpl(DeleteWndCmd &cmd) +{ +#ifdef GLVIS_USE_EGL + if (cmd.handle->ctx != EGL_NO_CONTEXT) + { + if (!eglDestroyContext(disp, cmd.handle->ctx)) + { + std::cerr << "Cannot destroy context, error: " << eglGetError() << std::endl; + return false; + } + cmd.handle->ctx = EGL_NO_CONTEXT; + } + + if (cmd.handle->surf != EGL_NO_SURFACE) + { + if (!eglDestroySurface(disp, cmd.handle->surf)) + { + std::cerr << "Cannot destroy surface, error: " << eglGetError() << std::endl; + return false; + } + cmd.handle->surf = EGL_NO_SURFACE; + } +#endif +#ifdef GLVIS_USE_CGL + if (cmd.handle->isInitialized()) + { + glDeleteFramebuffers(1, &cmd.handle->buf_frame); + glDeleteRenderbuffers(1, &cmd.handle->buf_color); + glDeleteRenderbuffers(1, &cmd.handle->buf_depth); + } + if (cmd.handle->pix) + { + CGLError err = CGLDestroyPixelFormat(cmd.handle->pix); + if (err != kCGLNoError) + { + std::cerr << "Cannot destroy pixel format, error: " + << CGLErrorString(err) << std::endl; + return false; + } + cmd.handle->pix = nullptr; + } +#endif + windows.remove(cmd.wnd); + num_windows--; + + return true; +} + +void EglMainThread::QueueWndCmd(CtrlCmd cmd, bool sync) +{ + future wait_complete; + if (sync) + { + wait_complete = cmd.finished.get_future(); + } + // queue up our event + { + lock_guard req_lock{window_cmd_mtx}; + window_cmds.emplace_back(std::move(cmd)); + } + // wake up the main thread to handle our event + events_available.notify_all(); + + if (sync) { wait_complete.get(); } +} + +void EglMainThread::InterruptHandler(int param) +{ + EglMainThread::Get().Terminate(); +} + +EglMainThread::EglMainThread() +{ +#ifdef GLVIS_USE_EGL + if (disp != EGL_NO_DISPLAY) + { + return; + } + + // Initialize EGL + disp = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (disp == EGL_NO_DISPLAY) + { + std::cerr << "FATAL: Failed to get an EGL display: " << eglGetError() << + std::endl; + return; + } + + EGLint major, minor; + + if (!eglInitialize(disp, &major, &minor)) + { + std::cerr << "FATAL: Failed to initialize EGL: " << eglGetError() << std::endl; + return; + } + + std::cout << "Using EGL " << major << "." << minor << std::endl; +#endif +#ifdef GLVIS_USE_CGL + GLint major, minor; + CGLGetVersion(&major, &minor); + std::cout << "Using CGL " << major << "." << minor << std::endl; +#endif +} + +EglMainThread::~EglMainThread() +{ +#ifdef GLVIS_USE_EGL + eglTerminate(disp); +#endif +} + +EglMainThread &EglMainThread::Get() +{ + static EglMainThread singleton; + return singleton; +} + +EglMainThread::Handle EglMainThread::CreateWindow(EglWindow *caller, int w, + int h, bool legacy_gl) +{ + CtrlCmd cmd; + cmd.type = CtrlCmdType::Create; + + CreateWndCmd crt_cmd; + cmd.create_cmd = &crt_cmd; + + crt_cmd.wnd = caller; + crt_cmd.w = w; + crt_cmd.h = h; + crt_cmd.legacy_gl = legacy_gl; + + auto res_handle = crt_cmd.out_handle.get_future(); + + QueueWndCmd(std::move(cmd), false); + + Handle out_hnd = res_handle.get(); + +#ifdef GLVIS_USE_EGL + if (out_hnd.surf == EGL_NO_SURFACE) + { + return out_hnd; + } + + // 3. Bind the API + if (!eglBindAPI(EGL_OPENGL_API)) + { + std::cerr << "Cannot bind OpenGL API, error: " << eglGetError() << std::endl; + return out_hnd; + } + + // 4. Create a context and make it current + if (legacy_gl) + { + // Try and probe for a core/compatibility context. Needed for Mac OS X, + // which will only support OpenGL 2.1 if you don't create a core context. + PRINT_DEBUG("Opening OpenGL core profile context..." << std::flush); + const EGLint attrListCore[] = + { + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_NONE + }; + out_hnd.ctx = eglCreateContext(disp, out_hnd.eglCfg, EGL_NO_CONTEXT, + attrListCore); + if (out_hnd.ctx == EGL_NO_CONTEXT) + { + PRINT_DEBUG("failed." << std::endl); + PRINT_DEBUG("Opening OpenGL compatibility profile context..." << std::flush); + const EGLint attrListCompat[] = + { + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT, + EGL_NONE + }; + out_hnd.ctx = eglCreateContext(disp, out_hnd.eglCfg, EGL_NO_CONTEXT, + attrListCompat); + if (out_hnd.ctx == EGL_NO_CONTEXT) + { + PRINT_DEBUG("failed." << std::endl); + } + else + { + PRINT_DEBUG("success!" << std::endl); + } + } + else + { + PRINT_DEBUG("success!" << std::endl); + } + } + + if (out_hnd.ctx == EGL_NO_CONTEXT) + { + PRINT_DEBUG("Opening OpenGL context with no flags..." << std::flush); + out_hnd.ctx = eglCreateContext(disp, out_hnd.eglCfg, EGL_NO_CONTEXT, + NULL); + if (out_hnd.ctx == EGL_NO_CONTEXT) + { + PRINT_DEBUG("failed." << std::endl); + std::cerr << "Cannot create an EGL context, error: " << eglGetError() << + std::endl; + + return out_hnd; + } + else + { + PRINT_DEBUG("success!" << std::endl); + } + } + + if (!eglMakeCurrent(disp, out_hnd.surf, out_hnd.surf, out_hnd.ctx)) + { + std::cerr << "Cannot set the EGL context as current, error: " << eglGetError() + << std::endl; + + return out_hnd; + } +#endif //GLVIS_USE_EGL +#ifdef GLVIS_USE_CGL + if (!out_hnd.isInitialized()) { return out_hnd; } + CGLError err = CGLSetCurrentContext(out_hnd.ctx.get()); + if (err != kCGLNoError) { return out_hnd; } + // initialize GLEW before using any OpenGL functions + caller->initGLEW(legacy_gl); + glGenFramebuffers(1, &out_hnd.buf_frame); + glGenRenderbuffers(1, &out_hnd.buf_color); + glGenRenderbuffers(1, &out_hnd.buf_depth); + ResizeWindow(out_hnd, w, h); +#endif + return out_hnd; +} + +void EglMainThread::ResizeWindow(Handle &handle, int w, int h) +{ + if (!handle.isInitialized()) { return; } + +#ifdef GLVIS_USE_EGL + CtrlCmd cmd; + cmd.type = CtrlCmdType::Resize; + + ResizeWndCmd res_cmd; + cmd.resize_cmd = &res_cmd; + + res_cmd.handle = &handle; + res_cmd.w = w; + res_cmd.h = h; + + QueueWndCmd(std::move(cmd), true); + + if (!eglMakeCurrent(disp, handle.surf, handle.surf, handle.ctx)) + { + std::cerr << "Cannot set the EGL context as current, error: " << eglGetError() + << std::endl; + return; + } +#endif +#ifdef GLVIS_USE_CGL + glBindRenderbuffer(GL_RENDERBUFFER, handle.buf_color); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, w, h); + glBindRenderbuffer(GL_RENDERBUFFER, handle.buf_depth); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, w, h); + glBindFramebuffer(GL_FRAMEBUFFER, handle.buf_frame); + glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, handle.buf_color); + glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, handle.buf_depth); + if (glGetError() != GL_NO_ERROR || + glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + std::cerr << "Cannot resize the framebuffer!" << std::endl; + } +#endif +} + +void EglMainThread::DeleteWindow(EglWindow *caller, Handle &handle) +{ + if (!handle.isInitialized()) { return; } + + CtrlCmd cmd; + cmd.type = CtrlCmdType::Delete; + + DeleteWndCmd del_cmd; + cmd.delete_cmd = &del_cmd; + + del_cmd.wnd = caller; + del_cmd.handle = &handle; + + QueueWndCmd(std::move(cmd), true); +} + +void EglMainThread::Terminate() +{ + CtrlCmd cmd; + cmd.type = CtrlCmdType::Terminate; + + // queue to the front to get priority + { + lock_guard req_lock{window_cmd_mtx}; + window_cmds.emplace_front(std::move(cmd)); + } + // wake up the main thread to handle our event + events_available.notify_all(); +} + +void EglMainThread::MainLoop(bool server) +{ + server_mode = server; + bool terminating = false; + + // set up interrupt handler for graceful closing + signal(SIGINT, InterruptHandler); + + while (true) + { + bool events_pending = false; + { + lock_guard evt_guard{window_cmd_mtx}; + events_pending = !window_cmds.empty(); + } + if (events_pending) + { + do + { + CtrlCmd cmd; + // Fetch next event from the queue + { + lock_guard evt_guard{window_cmd_mtx}; + cmd = std::move(window_cmds.front()); + window_cmds.pop_front(); + events_pending = !window_cmds.empty(); + // Skip non-delete events if terminating + if (terminating && cmd.type != CtrlCmdType::Delete) + { + continue; + } + } + + switch (cmd.type) + { + case CtrlCmdType::Create: + if (!CreateWndImpl(*cmd.create_cmd)) + { + terminating = true; + } + break; + case CtrlCmdType::Resize: + if (!ResizeWndImpl(*cmd.resize_cmd)) + { + terminating = true; + } + break; + case CtrlCmdType::Delete: + if (!DeleteWndImpl(*cmd.delete_cmd)) + { + terminating = true; + } + break; + case CtrlCmdType::Terminate: + // do not wait for windows to open + if (num_windows < 0) { num_windows = 0; } + terminating = true; + break; + } + + // Signal completion of the command, in case worker thread is waiting. + cmd.finished.set_value(); + } + while (events_pending); + } + + if (num_windows == 0) + { + if (!server_mode || terminating) + { + break; + } + } + + if (terminating) + { + for (EglWindow *wnd : windows) + { + wnd->signalQuit(); + } + } + + { + unique_lock event_lock{window_cmd_mtx}; + events_available.wait(event_lock, [this]() + { + // Sleep until events from windows can be handled + return !window_cmds.empty(); + }); + } + } +} + +#endif // GLVIS_USE_EGL || GLVIS_USE_CGL diff --git a/lib/egl/egl_main.hpp b/lib/egl/egl_main.hpp new file mode 100644 index 00000000..ef33a52f --- /dev/null +++ b/lib/egl/egl_main.hpp @@ -0,0 +1,88 @@ +// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced +// at the Lawrence Livermore National Laboratory. All Rights reserved. See files +// LICENSE and NOTICE for details. LLNL-CODE-443271. +// +// This file is part of the GLVis visualization tool and library. For more +// information and source code availability see https://glvis.org. +// +// GLVis is free software; you can redistribute it and/or modify it under the +// terms of the BSD-3 license. We welcome feedback and contributions, see file +// CONTRIBUTING.md for details. + +#ifndef GLVIS_EGL_MAIN_HPP +#define GLVIS_EGL_MAIN_HPP + +#if defined(GLVIS_USE_EGL) || defined(GLVIS_USE_CGL) + +#include +#include + +#include "egl.hpp" + +class EglMainThread : public MainThread +{ + using Handle = EglWindow::Handle; +#ifdef GLVIS_USE_EGL + EGLDisplay disp {EGL_NO_DISPLAY}; +#endif + bool server_mode {false}; + + std::list windows; + int num_windows {-1}; + + struct CreateWndCmd; + struct ResizeWndCmd; + struct DeleteWndCmd; + + enum class CtrlCmdType + { + Create, + Resize, + Delete, + Terminate, + }; + + struct CtrlCmd + { + CtrlCmdType type; + union + { + CreateWndCmd *create_cmd; + ResizeWndCmd *resize_cmd; + DeleteWndCmd *delete_cmd; + }; + + std::promise finished; + }; + + std::condition_variable events_available; + std::mutex window_cmd_mtx; + std::deque window_cmds; + + bool CreateWndImpl(CreateWndCmd &cmd); + bool ResizeWndImpl(ResizeWndCmd &cmd); + bool DeleteWndImpl(DeleteWndCmd &cmd); + void QueueWndCmd(CtrlCmd cmd, bool sync); + + static void InterruptHandler(int param); + +public: + + EglMainThread(); + ~EglMainThread(); + + static EglMainThread& Get(); +#ifdef GLVIS_USE_EGL + EGLDisplay GetDisplay() const { return disp; } +#endif + + Handle CreateWindow(EglWindow *caller, int w, int h, bool legacy_gl); + void ResizeWindow(Handle &hnd, int w, int h); + void DeleteWindow(EglWindow *caller, Handle &hnd); + void Terminate(); + + void MainLoop(bool server = false) override; +}; + +#endif // GLVIS_USE_EGL || GLVIS_USE_CGL +#endif // GLVIS_EGL_MAIN_HPP diff --git a/lib/glwindow.cpp b/lib/glwindow.cpp new file mode 100644 index 00000000..1a6f00db --- /dev/null +++ b/lib/glwindow.cpp @@ -0,0 +1,132 @@ +// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced +// at the Lawrence Livermore National Laboratory. All Rights reserved. See files +// LICENSE and NOTICE for details. LLNL-CODE-443271. +// +// This file is part of the GLVis visualization tool and library. For more +// information and source code availability see https://glvis.org. +// +// GLVis is free software; you can redistribute it and/or modify it under the +// terms of the BSD-3 license. We welcome feedback and contributions, see file +// CONTRIBUTING.md for details. + +#include "glwindow.hpp" +#include "aux_vis.hpp" +#include "gl/renderer_core.hpp" +#include "gl/renderer_ff.hpp" + +#ifdef GLVIS_DEBUG +#define PRINT_DEBUG(s) std::cerr << s +#else +#define PRINT_DEBUG(s) {} +#endif + +bool GLWindow::initGLEW(bool legacyGlOnly) +{ + GLenum err = glewInit(); +#ifdef GLEW_ERROR_NO_GLX_DISPLAY + // NOTE: Hacky workaround for Wayland initialization failure + // See https://github.com/nigels-com/glew/issues/172 + if (err == GLEW_ERROR_NO_GLX_DISPLAY) + { + std::cerr << "GLEW: No GLX display found. If you are using Wayland this can " + << "be ignored." << std::endl; + err = GLEW_OK; + } +#endif + if (err != GLEW_OK) + { + std::cerr << "FATAL: Failed to initialize GLEW: " + << glewGetErrorString(err) << std::endl; + return false; + } + + // print versions + PRINT_DEBUG("Using GLEW " << glewGetString(GLEW_VERSION) << std::endl); + PRINT_DEBUG("Using GL " << glGetString(GL_VERSION) << std::endl); + + renderer.reset(new gl3::MeshRenderer); + renderer->setSamplesMSAA(GetMultisample()); +#ifndef __EMSCRIPTEN__ + if (!GLEW_VERSION_1_1) + { + std::cerr << "FATAL: Minimum of OpenGL 1.1 is required." << std::endl; + return false; + } + if (!GLEW_VERSION_1_3) + { + // Multitexturing was introduced into the core OpenGL specification in + // version 1.3; for versions before, we need to load the functions from + // the ARB_multitexture extension. + if (GLEW_ARB_multitexture) + { + glActiveTexture = glActiveTextureARB; + glClientActiveTexture = glClientActiveTextureARB; + glMultiTexCoord2f = glMultiTexCoord2fARB; + } + else + { + std::cerr << "FATAL: Missing OpenGL multitexture support." << std::endl; + return false; + } + } + if (!GLEW_VERSION_3_0 && GLEW_EXT_transform_feedback) + { + glBindBufferBase = glBindBufferBaseEXT; + // Use an explicit typecast to suppress an error from inconsistent types + // that are present in older versions of GLEW. + glTransformFeedbackVaryings = + (PFNGLTRANSFORMFEEDBACKVARYINGSPROC)glTransformFeedbackVaryingsEXT; + glBeginTransformFeedback = glBeginTransformFeedbackEXT; + glEndTransformFeedback = glEndTransformFeedbackEXT; + } + if (!legacyGlOnly && (GLEW_VERSION_3_0 + || (GLEW_VERSION_2_0 && GLEW_EXT_transform_feedback))) + { + // We require both shaders and transform feedback EXT_transform_feedback + // was made core in OpenGL 3.0 + PRINT_DEBUG("Loading CoreGLDevice..." << std::endl); + renderer->setDevice(); + } + else + { + PRINT_DEBUG("Shader support missing, loading FFGLDevice..." << std::endl); + renderer->setDevice(); + } + +#else + renderer->setDevice(); +#endif + + if ((err = glGetError()) != GL_NO_ERROR) + { + std::cerr << "Renderer init OpenGL error: " << err << std::endl; + return false; + } + + return true; +} + +void GLWindow::recordKey(SDL_Keycode sym, SDL_Keymod mod) +{ + // Record the key in 'saved_keys': + bool isAlt = mod & (KMOD_ALT); + bool isCtrl = mod & (KMOD_CTRL); + if (isAlt || isCtrl) + { + saved_keys += "["; + } + if (isCtrl) { saved_keys += "C-"; } + if (isAlt) { saved_keys += "Alt-"; } + if (sym >= 32 && sym < 127) + { + saved_keys += (char)(sym); + } + else + { + saved_keys += SDL_GetKeyName(sym); + } + if (isAlt || isCtrl) + { + saved_keys += "]"; + } +} diff --git a/lib/glwindow.hpp b/lib/glwindow.hpp new file mode 100644 index 00000000..b0da9d50 --- /dev/null +++ b/lib/glwindow.hpp @@ -0,0 +1,186 @@ +// Copyright (c) 2010-2025, Lawrence Livermore National Security, LLC. Produced +// at the Lawrence Livermore National Laboratory. All Rights reserved. See files +// LICENSE and NOTICE for details. LLNL-CODE-443271. +// +// This file is part of the GLVis visualization tool and library. For more +// information and source code availability see https://glvis.org. +// +// GLVis is free software; you can redistribute it and/or modify it under the +// terms of the BSD-3 license. We welcome feedback and contributions, see file +// CONTRIBUTING.md for details. + +#ifndef GLVIS_GLWINDOW_HPP +#define GLVIS_GLWINDOW_HPP + +#include +#include +#include +#include + +#include "gl/renderer.hpp" + +class MainThread +{ +public: + virtual ~MainThread() { } + + // Handles all operations that are expected to be handled on the main + // thread (i.e. events and window creation) + virtual void MainLoop(bool server_mode) = 0; +}; + +class GLWindow +{ + friend class EglMainThread; +public: + typedef Uint8 SDL_Mousebutton; + struct MouseEventInfo + { + Sint32 mouse_x; + Sint32 mouse_y; + SDL_Keymod keymod; + }; + + using IdleDelegate = bool (*)(); + using Delegate = void (*)(); + using KeyDelegate = std::function; + using MouseDelegate = void (*)(MouseEventInfo*); + +protected: + enum class RenderState + { + // window displayed is fully current (no events or backbuffer updates pending) + Updated, + // events issued which may require a call to MyExpose + ExposePending, + // back buffer updated by MyExpose, now awaiting swap to be displayed on window + SwapPending + }; + RenderState wnd_state{RenderState::Updated}; + + std::unique_ptr renderer; + + IdleDelegate onIdle{}; + Delegate onExpose{}; + std::map onKeyDown; + std::map onMouseDown; + std::map onMouseUp; + std::map onMouseMove; + +#ifdef __EMSCRIPTEN__ + std::string canvas_id; +#endif + std::string saved_keys; + + bool initGLEW(bool legacyGlOnly); + void recordKey(SDL_Keycode k, SDL_Keymod m); + +public: + virtual ~GLWindow() = default; + + /// Creates a new OpenGL window + virtual bool createWindow(const char *title, int x, int y, int w, int h, + bool legacyGlOnly) = 0; + + /// Returns the renderer object + inline gl3::MeshRenderer& getRenderer() { return *renderer.get(); } + + /// Runs the window loop. + virtual void mainLoop() = 0; + virtual void mainIter() = 0; + + /// Signals addition of a new event + virtual void signalLoop() = 0; + + void setOnIdle(IdleDelegate func) { onIdle = func; } + void setOnExpose(Delegate func) { onExpose = func; } + + void setOnKeyDown(SDL_Keycode key, Delegate func) + { + onKeyDown[key] = [func](SDL_Keymod) { func(); }; + } + void setOnKeyDown(SDL_Keycode key, KeyDelegate func) { onKeyDown[key] = func; } + + void setOnMouseDown(SDL_Mousebutton btn, MouseDelegate func) { onMouseDown[btn] = func; } + void setOnMouseUp(SDL_Mousebutton btn, MouseDelegate func) { onMouseUp[btn] = func; } + void setOnMouseMove(SDL_Mousebutton btn, MouseDelegate func) { onMouseMove[btn] = func; } + + virtual void clearEvents() + { + onIdle = nullptr; + onExpose = nullptr; + onKeyDown.clear(); + onMouseUp.clear(); + onMouseDown.clear(); + onMouseMove.clear(); + } + + void callKeyDown(SDL_Keycode k, SDL_Keymod mod = KMOD_NONE) + { + if (onKeyDown[k]) + { + onKeyDown[k](mod); + } + } + + /// Returns size of the window + virtual void getWindowSize(int& w, int& h) const { w = h = 0; } + + /// Returns the drawable size + virtual void getGLDrawSize(int& w, int& h) const { w = h = 0; } + + /// Returns the resolution (DPI) of the display + virtual void getDpi(int& wdpi, int& hdpi) const { wdpi = hdpi = 0; } + + /// Checks if the display has high resolution (DPI) + virtual bool isHighDpi() const { return false; } + + /// Set title of the window (string version) + virtual void setWindowTitle(const std::string& title) { } + + /// Set title of the window (C-string version) + virtual void setWindowTitle(const char* title) { } + + /// Set window size + virtual void setWindowSize(int w, int h) { } + + /// Set window position + virtual void setWindowPos(int x, int y) { } + + /// Returns true if the window has been succesfully initialized + virtual bool isWindowInitialized() const { return false; } + + /// Returns true if the OpenGL context was successfully initialized + virtual bool isGlInitialized() const { return false; } + + /// Signals key down event + virtual void signalKeyDown(SDL_Keycode k, SDL_Keymod m = KMOD_NONE) { } + + /// Signals quit event + virtual void signalQuit() { } + + /// Signals expose event when objects have been updated + virtual void signalExpose() { wnd_state = RenderState::ExposePending; } + + /// Signals swap event when the back buffer is ready for swapping + virtual void signalSwap() { wnd_state = RenderState::SwapPending; } + + /// Checks if the expose event is pending + virtual bool isExposePending() const { return wnd_state == RenderState::ExposePending; } + + /// Checks if the swap event is pending + virtual bool isSwapPending() const { return wnd_state == RenderState::SwapPending; } + + /// Returns the keyboard events that have been logged by the window. + std::string getSavedKeys() const { return saved_keys; } + + /// Saves a screenshot ot the file, performing conversion optionally + virtual void screenshot(std::string filename, bool convert = false) { } + +#ifdef __EMSCRIPTEN__ + std::string getCanvasId() const { return canvas_id; } + void setCanvasId(std::string canvas_id_) { canvas_id = canvas_id_; } +#endif +}; + +#endif //GLVIS_GLWINDOW_HPP diff --git a/lib/openglvis.cpp b/lib/openglvis.cpp index 9b066a38..c092dddb 100644 --- a/lib/openglvis.cpp +++ b/lib/openglvis.cpp @@ -113,7 +113,7 @@ void Camera::Print() << std::endl; } -VisualizationScene::VisualizationScene(SdlWindow &wnd_) +VisualizationScene::VisualizationScene(GLWindow &wnd_) { wnd = &wnd_; diff --git a/lib/openglvis.hpp b/lib/openglvis.hpp index 63c33624..0311d13e 100644 --- a/lib/openglvis.hpp +++ b/lib/openglvis.hpp @@ -16,7 +16,7 @@ #include "material.hpp" #include "palettes.hpp" #include "geom_utils.hpp" -#include "sdl.hpp" +#include "glwindow.hpp" #include "gltf.hpp" // Visualization header file @@ -63,7 +63,7 @@ class VisualizationScene // How to scale the visualized object(s) double xscale, yscale, zscale; - SdlWindow * wnd; + GLWindow *wnd; glm::mat4 proj_mtx; @@ -169,7 +169,7 @@ class VisualizationScene const gl3::GlDrawable &gl_drawable); public: - VisualizationScene(SdlWindow &wnd); + VisualizationScene(GLWindow &wnd); virtual ~VisualizationScene(); int spinning, OrthogonalProjection, print, movie; diff --git a/lib/palettes.cpp b/lib/palettes.cpp index bd8110d8..a6d33830 100644 --- a/lib/palettes.cpp +++ b/lib/palettes.cpp @@ -168,7 +168,8 @@ void PaletteState::SetIndex(int num) if ((num >= 0) && (num < Palettes->NumPalettes())) { curr_palette = num; - cout << "Palette: " << num+1 << ") " << Palettes->Get(curr_palette)->name << endl; + cout << "Palette: " << num+1 << ") " << Palettes->Get(curr_palette)->name + << endl; } else { @@ -182,7 +183,8 @@ void PaletteState::SetByName(const std::string& palette_name) if ((num >= 0) && (num < Palettes->NumPalettes())) { curr_palette = num; - cout << "Palette: " << num+1 << ") " << Palettes->Get(curr_palette)->name << endl; + cout << "Palette: " << num+1 << ") " << Palettes->Get(curr_palette)->name + << endl; } else { @@ -196,7 +198,8 @@ bool PaletteState::UseDefaultIndex() if ((num >= 0) && (num < Palettes->NumPalettes())) { curr_palette = num; - cout << "Palette: " << num+1 << ") " << Palettes->Get(curr_palette)->name << endl; + cout << "Palette: " << num+1 << ") " << Palettes->Get(curr_palette)->name + << endl; return true; } return false; diff --git a/lib/script_controller.cpp b/lib/script_controller.cpp index d3f1c799..edca7707 100644 --- a/lib/script_controller.cpp +++ b/lib/script_controller.cpp @@ -14,6 +14,8 @@ #include "coll_reader.hpp" #include "stream_reader.hpp" #include "visual.hpp" +#include "sdl/sdl.hpp" +#include "egl/egl.hpp" #include #include @@ -59,6 +61,7 @@ enum class Command Scale, Translate, PlotCaption, + Headless, //---------- Max }; @@ -122,6 +125,7 @@ ScriptCommands::ScriptCommands() (*this)[Command::Scale] = {"scale", "", "Set the scaling factor."}; (*this)[Command::Translate] = {"translate", " ", "Set the translation coordinates."}; (*this)[Command::PlotCaption] = {"plot_caption", "''", "Set the plot caption."}; + (*this)[Command::Headless] = {"headless", "", "Change the session to headless."}; } int ScriptController::ScriptReadSolution(istream &scr, DataState &state) @@ -353,12 +357,12 @@ void ScriptController::PrintCommands() } } -void ScriptController::ExecuteScriptCommand() +bool ScriptController::ExecuteScriptCommand() { if (!script) { cout << "No script stream defined! (Bug?)" << endl; - return; + return false; } istream &scr = *script; @@ -371,7 +375,11 @@ void ScriptController::ExecuteScriptCommand() { cout << "End of script." << endl; scr_level = 0; - return; + if (win.headless) + { + win.wnd->signalQuit(); + } + return false; } if (scr.peek() == '#') { @@ -399,7 +407,7 @@ void ScriptController::ExecuteScriptCommand() { cout << "Unknown command in script: " << word << endl; PrintCommands(); - break; + return false; } const Command cmd = (Command)(it - commands.begin()); @@ -504,15 +512,10 @@ void ScriptController::ExecuteScriptCommand() { scr >> ws >> word; - cout << "Script: screenshot: " << flush; - - if (Screenshot(word.c_str(), true)) - { - cout << "Screenshot(" << word << ") failed." << endl; - done_one_command = 1; - continue; - } - cout << "-> " << word << endl; + cout << "Script: screenshot: -> " << word << endl; + // Allow GlWindow to handle the expose and screenshot action, in case + // any actions need to be taken before MyExpose(). + win.wnd->screenshot(word, true); if (scr_min_val > win.vs->GetMinV()) { @@ -840,12 +843,16 @@ void ScriptController::ExecuteScriptCommand() MyExpose(); } break; + case Command::Headless: + cout << "The session cannot become headless after initialization" << endl; + break; case Command::Max: //dummy break; } done_one_command = 1; } + return true; } thread_local ScriptController *ScriptController::script_ctrl = NULL; @@ -853,7 +860,7 @@ thread_local ScriptController *ScriptController::script_ctrl = NULL; void ScriptController::ScriptIdleFunc() { script_ctrl->ExecuteScriptCommand(); - if (script_ctrl->scr_level == 0) + if (script_ctrl->scr_level == 0 && !script_ctrl->win.headless) { ScriptControl(); } @@ -914,6 +921,9 @@ void ScriptController::PlayScript(Window win, istream &scr) scr >> script.win.window_x >> script.win.window_y >> script.win.window_w >> script.win.window_h; break; + case Command::Headless: + script.win.headless = true; + break; case Command::DataCollCycle: scr >> script.dc_cycle; break; @@ -986,9 +996,12 @@ void ScriptController::PlayScript(Window win, istream &scr) script.script = &scr; script.win.data_state.keys.clear(); - // Make sure the singleton object returned by GetMainThread() is + // backup the headless flag as the window is moved + const bool headless = script.win.headless; + + // Make sure the returned singleton object is // initialized from the main thread. - GetMainThread(); + GetMainThread(headless); std::thread worker_thread { @@ -997,13 +1010,21 @@ void ScriptController::PlayScript(Window win, istream &scr) script_ctrl = &local_script; if (local_script.win.GLVisInitVis({})) { - local_script.win.wnd->setOnKeyDown(SDLK_SPACE, ScriptControl); + if (!local_script.win.headless) + { + local_script.win.wnd->setOnKeyDown(SDLK_SPACE, ScriptControl); + } + else + { + // execute all commands, updating the scene every time + ScriptControl(); + } local_script.win.GLVisStartVis(); } }, std::move(script) }; - SDLMainLoop(); + MainThreadLoop(headless); worker_thread.join(); } diff --git a/lib/script_controller.hpp b/lib/script_controller.hpp index eb3e3d62..04d1d833 100644 --- a/lib/script_controller.hpp +++ b/lib/script_controller.hpp @@ -47,7 +47,7 @@ class ScriptController static void ScriptControl(); static void PrintCommands(); - void ExecuteScriptCommand(); + bool ExecuteScriptCommand(); public: ScriptController(Window win_) : win(std::move(win_)) { } diff --git a/lib/sdl.cpp b/lib/sdl/sdl.cpp similarity index 72% rename from lib/sdl.cpp rename to lib/sdl/sdl.cpp index c733102b..b45dc823 100644 --- a/lib/sdl.cpp +++ b/lib/sdl/sdl.cpp @@ -10,9 +10,7 @@ // CONTRIBUTING.md for details. #include -#include "aux_vis.hpp" -#include "gl/renderer_core.hpp" -#include "gl/renderer_ff.hpp" +#include "../aux_vis.hpp" #include "sdl.hpp" #include "sdl_main.hpp" @@ -67,13 +65,13 @@ SdlWindow::Handle::~Handle() } } -SdlMainThread& GetMainThread() +SdlMainThread& GetSdlMainThread() { static SdlMainThread inst; return inst; } -bool SdlWindow::isGlInitialized() +bool SdlWindow::isGlInitialized() const { return (handle.gl_ctx != 0); } @@ -82,7 +80,7 @@ SdlWindow::SdlWindow() {} void SdlWindow::StartSDL(bool server_mode) { - GetMainThread().MainLoop(server_mode); + GetSdlMainThread().MainLoop(server_mode); } const int default_dpi = 72; @@ -94,7 +92,7 @@ bool SdlWindow::createWindow(const char* title, int x, int y, int w, int h, is_multithreaded = false; #endif // create a new SDL window - handle = GetMainThread().GetHandle(this, title, x, y, w, h, legacyGlOnly); + handle = GetSdlMainThread().GetHandle(this, title, x, y, w, h, legacyGlOnly); // at this point, window should be up if (!handle.isInitialized()) @@ -104,88 +102,13 @@ bool SdlWindow::createWindow(const char* title, int x, int y, int w, int h, window_id = SDL_GetWindowID(handle.hwnd); - GLenum err = glewInit(); -#ifdef GLEW_ERROR_NO_GLX_DISPLAY - // NOTE: Hacky workaround for Wayland initialization failure - // See https://github.com/nigels-com/glew/issues/172 - if (err == GLEW_ERROR_NO_GLX_DISPLAY) - { - cerr << "GLEW: No GLX display found. If you are using Wayland this can " - << "be ignored." << endl; - err = GLEW_OK; - } -#endif - if (err != GLEW_OK) - { - cerr << "FATAL: Failed to initialize GLEW: " - << glewGetErrorString(err) << endl; - return false; - } - - // print versions - PRINT_DEBUG("Using GLEW " << glewGetString(GLEW_VERSION) << std::endl); - PRINT_DEBUG("Using GL " << glGetString(GL_VERSION) << std::endl); - - renderer.reset(new gl3::MeshRenderer); - renderer->setSamplesMSAA(GetMultisample()); -#ifndef __EMSCRIPTEN__ - if (!GLEW_VERSION_1_1) - { - cerr << "FATAL: Minimum of OpenGL 1.1 is required." << endl; - return false; - } - if (!GLEW_VERSION_1_3) - { - // Multitexturing was introduced into the core OpenGL specification in - // version 1.3; for versions before, we need to load the functions from - // the ARB_multitexture extension. - if (GLEW_ARB_multitexture) - { - glActiveTexture = glActiveTextureARB; - glClientActiveTexture = glClientActiveTextureARB; - glMultiTexCoord2f = glMultiTexCoord2fARB; - } - else - { - cerr << "FATAL: Missing OpenGL multitexture support." << endl; - return false; - } - } - if (!GLEW_VERSION_3_0 && GLEW_EXT_transform_feedback) - { - glBindBufferBase = glBindBufferBaseEXT; - // Use an explicit typecast to suppress an error from inconsistent types - // that are present in older versions of GLEW. - glTransformFeedbackVaryings = - (PFNGLTRANSFORMFEEDBACKVARYINGSPROC)glTransformFeedbackVaryingsEXT; - glBeginTransformFeedback = glBeginTransformFeedbackEXT; - glEndTransformFeedback = glEndTransformFeedbackEXT; - } - if (!legacyGlOnly && (GLEW_VERSION_3_0 - || (GLEW_VERSION_2_0 && GLEW_EXT_transform_feedback))) - { - // We require both shaders and transform feedback EXT_transform_feedback - // was made core in OpenGL 3.0 - PRINT_DEBUG("Loading CoreGLDevice..." << endl); - renderer->setDevice(); - } - else - { - PRINT_DEBUG("Shader support missing, loading FFGLDevice..." << endl); - renderer->setDevice(); - } - -#else - renderer->setDevice(); -#endif - - return true; + return initGLEW(legacyGlOnly); } SdlWindow::~SdlWindow() { // Let the main SDL thread delete the handles - GetMainThread().DeleteHandle(std::move(handle)); + GetSdlMainThread().DeleteHandle(std::move(handle)); } void SdlWindow::windowEvent(SDL_WindowEvent& ew) @@ -213,7 +136,7 @@ void SdlWindow::windowEvent(SDL_WindowEvent& ew) void SdlWindow::motionEvent(SDL_MouseMotionEvent& em) { - EventInfo info = + MouseEventInfo info = { em.x, em.y, SDL_GetModState() @@ -245,7 +168,7 @@ void SdlWindow::mouseEventDown(SDL_MouseButtonEvent& eb) { if (onMouseDown[eb.button]) { - EventInfo info = + MouseEventInfo info = { eb.x, eb.y, SDL_GetModState() @@ -258,7 +181,7 @@ void SdlWindow::mouseEventUp(SDL_MouseButtonEvent& eb) { if (onMouseUp[eb.button]) { - EventInfo info = + MouseEventInfo info = { eb.x, eb.y, SDL_GetModState() @@ -278,7 +201,7 @@ void SdlWindow::keyDownEvent(SDL_Keysym& ks) && (ks.mod & (KMOD_CTRL | KMOD_LALT | KMOD_GUI)) == 0) { lastKeyDownProcessed = false; - lastKeyDownMods = ks.mod; + lastKeyDownMods = (SDL_Keymod)ks.mod; lastKeyDownChar = ks.sym; return; } @@ -288,23 +211,9 @@ void SdlWindow::keyDownEvent(SDL_Keysym& ks) lastKeyDownProcessed = true; if (onKeyDown[ks.sym]) { - onKeyDown[ks.sym](ks.mod); + onKeyDown[ks.sym]((SDL_Keymod)ks.mod); - // Record the key in 'saved_keys': - bool isAlt = ks.mod & (KMOD_ALT); - bool isCtrl = ks.mod & (KMOD_CTRL); - saved_keys += "["; - if (isCtrl) { saved_keys += "C-"; } - if (isAlt) { saved_keys += "Alt-"; } - if (ks.sym >= 32 && ks.sym < 127) - { - saved_keys += (char)(ks.sym); - } - else - { - saved_keys += SDL_GetKeyName(ks.sym); - } - saved_keys += "]"; + recordKey(ks.sym, (SDL_Keymod)ks.mod); } } @@ -322,22 +231,10 @@ void SdlWindow::textInputEvent(const SDL_TextInputEvent &tie) } if (onKeyDown[c]) { - onKeyDown[c](lastKeyDownMods & ~(KMOD_CAPS | KMOD_LSHIFT | KMOD_RSHIFT)); + onKeyDown[c]((SDL_Keymod)(lastKeyDownMods & ~(KMOD_CAPS | KMOD_LSHIFT | + KMOD_RSHIFT))); - // Record the key in 'saved_keys': - bool isAlt = lastKeyDownMods & (KMOD_ALT); - bool isCtrl = lastKeyDownMods & (KMOD_CTRL); - if (isAlt || isCtrl) - { - saved_keys += "["; - } - if (isCtrl) { saved_keys += "C-"; } - if (isAlt) { saved_keys += "Alt-"; } - saved_keys += c; - if (isAlt || isCtrl) - { - saved_keys += "]"; - } + recordKey(c, lastKeyDownMods); } } @@ -361,8 +258,8 @@ void SdlWindow::mainIter() { if (!is_multithreaded) { - // Pull events from GetMainThread() object - GetMainThread().DispatchSDLEvents(); + // Pull events from GetSdlMainThread() object + GetSdlMainThread().DispatchSDLEvents(); } bool events_pending = false; bool sleep = false; @@ -468,7 +365,7 @@ void SdlWindow::mainIter() // To avoid this issue, we just call [NSOpenGLContext update] // immediately before the expose event. SdlCocoaPlatform* platform = - dynamic_cast(GetMainThread().GetPlatform()); + dynamic_cast(GetSdlMainThread().GetPlatform()); if (platform) { platform->ContextUpdate(); @@ -514,7 +411,7 @@ void SdlWindow::mainLoop() // TODO: Temporary workaround - after merge, everyone should update to // latest SDL SdlCocoaPlatform* mac_platform - = dynamic_cast(GetMainThread().GetPlatform()); + = dynamic_cast(GetSdlMainThread().GetPlatform()); if (mac_platform && mac_platform->UseThreadWorkaround()) { mac_platform->SwapWindow(); @@ -542,20 +439,20 @@ void SdlWindow::signalLoop() events_available.notify_all(); } -void SdlWindow::getWindowSize(int& w, int& h) +void SdlWindow::getWindowSize(int& w, int& h) const { w = 0; h = 0; if (handle.isInitialized()) { #ifdef __EMSCRIPTEN__ - if (canvas_id_.empty()) + if (canvas_id.empty()) { - std::cerr << "error: id is undefined: " << canvas_id_ << std::endl; + std::cerr << "error: id is undefined: " << canvas_id << std::endl; return; } double dw, dh; - auto err = emscripten_get_element_css_size(canvas_id_.c_str(), &dw, &dh); + auto err = emscripten_get_element_css_size(canvas_id.c_str(), &dw, &dh); w = int(dw); h = int(dh); if (err != EMSCRIPTEN_RESULT_SUCCESS) @@ -571,12 +468,12 @@ void SdlWindow::getWindowSize(int& w, int& h) } } -void SdlWindow::getGLDrawSize(int& w, int& h) +void SdlWindow::getGLDrawSize(int& w, int& h) const { SDL_GL_GetDrawableSize(handle.hwnd, &w, &h); } -void SdlWindow::getDpi(int& w, int& h) +void SdlWindow::getDpi(int& w, int& h) const { w = default_dpi; h = default_dpi; @@ -609,19 +506,19 @@ void SdlWindow::getDpi(int& w, int& h) } } -void SdlWindow::setWindowTitle(std::string& title) +void SdlWindow::setWindowTitle(const std::string& title) { setWindowTitle(title.c_str()); } void SdlWindow::setWindowTitle(const char * title) { - GetMainThread().SetWindowTitle(handle, title); + GetSdlMainThread().SetWindowTitle(handle, title); } void SdlWindow::setWindowSize(int w, int h) { - GetMainThread().SetWindowSize(handle, pixel_scale_x*w, pixel_scale_y*h); + GetSdlMainThread().SetWindowSize(handle, pixel_scale_x*w, pixel_scale_y*h); update_before_expose = true; } @@ -632,9 +529,9 @@ void SdlWindow::setWindowPos(int x, int y) SDL_WINDOWPOS_ISCENTERED(x); bool uc_y = SDL_WINDOWPOS_ISUNDEFINED(y) || SDL_WINDOWPOS_ISCENTERED(y); - GetMainThread().SetWindowPosition(handle, - uc_x ? x : pixel_scale_x*x, - uc_y ? y : pixel_scale_y*y); + GetSdlMainThread().SetWindowPosition(handle, + uc_x ? x : pixel_scale_x*x, + uc_y ? y : pixel_scale_y*y); update_before_expose = true; } diff --git a/lib/sdl.hpp b/lib/sdl/sdl.hpp similarity index 57% rename from lib/sdl.hpp rename to lib/sdl/sdl.hpp index 47e51b0e..a7f3e039 100644 --- a/lib/sdl.hpp +++ b/lib/sdl/sdl.hpp @@ -13,32 +13,19 @@ #define GLVIS_SDL_HPP #include -#include -#include -#include #include #include #include -#include "gl/renderer.hpp" +#include "../glwindow.hpp" +#include "sdl_helper.hpp" -struct EventInfo -{ - GLint mouse_x; - GLint mouse_y; - SDL_Keymod keymod; -}; - -using TouchDelegate = void (*)(SDL_MultiGestureEvent&); -using MouseDelegate = void (*)(EventInfo*); -using KeyDelegate = std::function; -using WindowDelegate = void (*)(int, int); -using Delegate = void (*)(); -using IdleDelegate = bool (*)(); +typedef void (*TouchDelegate)(SDL_MultiGestureEvent&); +typedef void (*WindowDelegate)(int, int); class SdlMainThread; -SdlMainThread& GetMainThread(); +SdlMainThread& GetSdlMainThread(); -class SdlWindow +class SdlWindow : public GLWindow { private: friend class SdlMainThread; @@ -76,7 +63,7 @@ class SdlWindow int window_id = -1; Handle handle; - std::unique_ptr renderer; + static const int high_dpi_threshold = 144; // The display is high-dpi when: // - SDL's "screen coordinates" sizes are different from the pixel sizes, or @@ -93,32 +80,11 @@ class SdlWindow bool running; - IdleDelegate onIdle{nullptr}; - Delegate onExpose{nullptr}; + WindowDelegate onReshape{nullptr}; - std::map onKeyDown; - std::map onMouseDown; - std::map onMouseUp; - std::map onMouseMove; TouchDelegate onTouchPinch{nullptr}; TouchDelegate onTouchRotate{nullptr}; -#ifdef __EMSCRIPTEN__ - std::string canvas_id_; -#endif - - enum class RenderState - { - // window displayed is fully current (no events or backbuffer updates pending) - Updated, - // events issued which may require a call to MyExpose - ExposePending, - // back buffer updated by MyExpose, now awaiting swap to be displayed on window - SwapPending - }; - - RenderState wnd_state{RenderState::Updated}; - bool update_before_expose{false}; // bool requiresExpose; @@ -126,7 +92,7 @@ class SdlWindow std::string screenshot_file; bool screenshot_convert; bool lastKeyDownProcessed; - Uint16 lastKeyDownMods; + SDL_Keymod lastKeyDownMods; char lastKeyDownChar; // internal event handlers @@ -156,8 +122,6 @@ class SdlWindow } } - std::string saved_keys; - std::condition_variable events_available; std::mutex event_mutex; // The window-specific events collected by the main event thread. @@ -170,76 +134,44 @@ class SdlWindow // thread only. static void StartSDL(bool server_mode); - /// Creates a new OpenGL window. Returns false if SDL or OpenGL initialization - /// fails. + /** @brief Creates a new OpenGL window. Returns false if SDL or OpenGL + initialization fails. */ bool createWindow(const char * title, int x, int y, int w, int h, - bool legacyGlOnly); + bool legacyGlOnly) override; /// Runs the window loop. - void mainLoop(); - void mainIter(); + void mainLoop() override; + void mainIter() override; // Called by worker threads in GLVisCommand::signal() - void signalLoop(); + void signalLoop() override; - void setOnIdle(IdleDelegate func) { onIdle = func; } - void setOnExpose(Delegate func) { onExpose = func; } void setOnReshape(WindowDelegate func) { onReshape = func; } - - void setOnKeyDown(int key, Delegate func) - { - onKeyDown[key] = [func](GLenum) { func(); }; - } - void setOnKeyDown(int key, KeyDelegate func) { onKeyDown[key] = func; } - - void setOnMouseDown(int btn, MouseDelegate func) { onMouseDown[btn] = func; } - void setOnMouseUp(int btn, MouseDelegate func) { onMouseUp[btn] = func; } - void setOnMouseMove(int btn, MouseDelegate func) { onMouseMove[btn] = func; } - void setTouchPinchCallback(TouchDelegate cb) { onTouchPinch = cb; } void setTouchRotateCallback(TouchDelegate cb) { onTouchRotate = cb; } - void clearEvents() + void clearEvents() override { - onIdle = nullptr; - onExpose = nullptr; + GLWindow::clearEvents(); onReshape = nullptr; - onKeyDown.clear(); - onMouseUp.clear(); - onMouseDown.clear(); - onMouseMove.clear(); } - void callKeyDown(SDL_Keycode k, Uint16 mod=0) - { - if (onKeyDown[k]) - { - onKeyDown[k](mod); - } - } - - void getWindowSize(int& w, int& h); - void getGLDrawSize(int& w, int& h); - void getDpi(int& wdpi, int& hdpi); + void getWindowSize(int& w, int& h) const override; + void getGLDrawSize(int& w, int& h) const override; + void getDpi(int& wdpi, int& hdpi) const override; /// This property is set by createWindow(). - bool isHighDpi() const { return high_dpi; } - - gl3::MeshRenderer& getRenderer() { return *renderer.get(); } - void setWindowTitle(std::string& title); - void setWindowTitle(const char* title); - void setWindowSize(int w, int h); - void setWindowPos(int x, int y); + bool isHighDpi() const override { return high_dpi; } - void signalKeyDown(SDL_Keycode k, SDL_Keymod m = KMOD_NONE); - void signalExpose() { wnd_state = RenderState::ExposePending; } - void signalSwap() { wnd_state = RenderState::SwapPending; } - void signalQuit() { running = false; } + void setWindowTitle(const std::string& title) override; + void setWindowTitle(const char* title) override; + void setWindowSize(int w, int h) override; + void setWindowPos(int x, int y) override; - /// Returns the keyboard events that have been logged by the window. - std::string getSavedKeys() const { return saved_keys; } + void signalKeyDown(SDL_Keycode k, SDL_Keymod m = KMOD_NONE) override; + void signalQuit() override { running = false; } /// Queues a screenshot to be taken. - void screenshot(std::string filename, bool convert = false) + void screenshot(std::string filename, bool convert = false) override { takeScreenshot = true; screenshot_file = filename; @@ -252,17 +184,9 @@ class SdlWindow void swapBuffer(); operator bool() { return handle.isInitialized(); } - bool isWindowInitialized() { return handle.isInitialized(); } + bool isWindowInitialized() const override { return handle.isInitialized(); } /// Returns true if the OpenGL context was successfully initialized. - bool isGlInitialized(); - - bool isSwapPending() { return wnd_state == RenderState::SwapPending; } - bool isExposePending() { return wnd_state == RenderState::ExposePending; } - -#ifdef __EMSCRIPTEN__ - std::string getCanvasId() const { return canvas_id_; } - void setCanvasId(std::string canvas_id) { canvas_id_ = canvas_id; } -#endif + bool isGlInitialized() const override; }; #endif diff --git a/lib/sdl_helper.cpp b/lib/sdl/sdl_helper.cpp similarity index 100% rename from lib/sdl_helper.cpp rename to lib/sdl/sdl_helper.cpp diff --git a/lib/sdl_helper.hpp b/lib/sdl/sdl_helper.hpp similarity index 97% rename from lib/sdl_helper.hpp rename to lib/sdl/sdl_helper.hpp index ac9310e1..965b7a66 100644 --- a/lib/sdl_helper.hpp +++ b/lib/sdl/sdl_helper.hpp @@ -12,7 +12,7 @@ #ifndef GLVIS_SDL_HELPER_HPP #define GLVIS_SDL_HELPER_HPP -#include "gl/platform_gl.hpp" +#include "../gl/platform_gl.hpp" #include class SdlNativePlatform diff --git a/lib/sdl_mac.hpp b/lib/sdl/sdl_mac.hpp similarity index 100% rename from lib/sdl_mac.hpp rename to lib/sdl/sdl_mac.hpp diff --git a/lib/sdl_mac.mm b/lib/sdl/sdl_mac.mm similarity index 100% rename from lib/sdl_mac.mm rename to lib/sdl/sdl_mac.mm diff --git a/lib/sdl_main.cpp b/lib/sdl/sdl_main.cpp similarity index 99% rename from lib/sdl_main.cpp rename to lib/sdl/sdl_main.cpp index 8d32d0ef..6ec3fbd6 100644 --- a/lib/sdl_main.cpp +++ b/lib/sdl/sdl_main.cpp @@ -15,7 +15,7 @@ #include "sdl_main.hpp" #include "sdl_helper.hpp" -#include "logo.hpp" +#include "../logo.hpp" #ifdef SDL_VIDEO_DRIVER_COCOA #include "sdl_mac.hpp" @@ -28,7 +28,7 @@ #endif extern int GetMultisample(); -extern bool wndUseHiDPI; +extern bool GetUseHiDPI(); using namespace std; @@ -618,7 +618,7 @@ void SdlMainThread::createWindowImpl(CreateWindowCmd& cmd) #ifndef __EMSCRIPTEN__ win_flags |= SDL_WINDOW_RESIZABLE; #endif - if (wndUseHiDPI) + if (GetUseHiDPI()) { win_flags |= SDL_WINDOW_ALLOW_HIGHDPI; } @@ -645,7 +645,11 @@ void SdlMainThread::createWindowImpl(CreateWindowCmd& cmd) #ifndef __EMSCRIPTEN__ SDL_GL_SetSwapInterval(0); +#ifdef SDL_VIDEO_DRIVER_COCOA + if (GLEW_KHR_debug) { glEnable(GL_DEBUG_OUTPUT); } +#else glEnable(GL_DEBUG_OUTPUT); +#endif #endif // Register window internally in the main thread so it can receive events @@ -680,7 +684,7 @@ void SdlMainThread::createWindowImpl(CreateWindowCmd& cmd) // Detect if we are using a high-dpi display and resize the window unless it // was already resized by SDL's underlying backend. - if (wndUseHiDPI) + if (GetUseHiDPI()) { SdlWindow* wnd = cmd.wnd; int scr_w, scr_h, pix_w, pix_h, wdpi, hdpi; diff --git a/lib/sdl_main.hpp b/lib/sdl/sdl_main.hpp similarity index 98% rename from lib/sdl_main.hpp rename to lib/sdl/sdl_main.hpp index 7a8a89f4..339448d2 100644 --- a/lib/sdl_main.hpp +++ b/lib/sdl/sdl_main.hpp @@ -23,7 +23,7 @@ #include "sdl.hpp" #include "sdl_helper.hpp" -class SdlMainThread +class SdlMainThread : public MainThread { private: using Handle = SdlWindow::Handle; @@ -39,7 +39,7 @@ class SdlMainThread // Handles all SDL operations that are expected to be handled on the main // SDL thread (i.e. events and window creation) - void MainLoop(bool server_mode); + void MainLoop(bool server_mode) override; // Dequeues all incoming events from SDL, and queues them up to their // matching windows. Intended to be called only in single-threaded mode. diff --git a/lib/sdl_windows.cpp b/lib/sdl/sdl_windows.cpp similarity index 100% rename from lib/sdl_windows.cpp rename to lib/sdl/sdl_windows.cpp diff --git a/lib/sdl_windows.hpp b/lib/sdl/sdl_windows.hpp similarity index 100% rename from lib/sdl_windows.hpp rename to lib/sdl/sdl_windows.hpp diff --git a/lib/sdl_x11.cpp b/lib/sdl/sdl_x11.cpp similarity index 100% rename from lib/sdl_x11.cpp rename to lib/sdl/sdl_x11.cpp diff --git a/lib/sdl_x11.hpp b/lib/sdl/sdl_x11.hpp similarity index 100% rename from lib/sdl_x11.hpp rename to lib/sdl/sdl_x11.hpp diff --git a/lib/threads.cpp b/lib/threads.cpp index c49fe693..441fcc0a 100644 --- a/lib/threads.cpp +++ b/lib/threads.cpp @@ -473,6 +473,20 @@ int GLVisCommand::Autopause(const char *mode) return 0; } +int GLVisCommand::Quit() +{ + if (lock() < 0) + { + return -1; + } + command = Command::QUIT; + if (signal() < 0) + { + return -2; + } + return 0; +} + int GLVisCommand::Execute() { if (!command_ready) @@ -536,7 +550,7 @@ int GLVisCommand::Execute() case Command::SCREENSHOT: { cout << "Command: screenshot -> " << screenshot_filename << endl; - // Allow SdlWindow to handle the expose and screenshot action, in case + // Allow GlWindow to handle the expose and screenshot action, in case // any actions need to be taken before MyExpose(). thread_wnd->screenshot(screenshot_filename, true); break; @@ -812,6 +826,11 @@ int GLVisCommand::Execute() break; } + case Command::QUIT: + { + thread_wnd->signalQuit(); + break; + } } command = Command::NO_COMMAND; @@ -939,8 +958,9 @@ ThreadCommands::ThreadCommands() } communication_thread::communication_thread(StreamCollection _is, - GLVisCommand* cmd) - : is(std::move(_is)), glvis_command(cmd) + GLVisCommand* cmd, + bool end_quit_) + : is(std::move(_is)), glvis_command(cmd), end_quit(end_quit_) { new_m = NULL; new_g = NULL; @@ -1576,6 +1596,11 @@ void communication_thread::execute() cout << "Stream: end of input." << endl; + if (end_quit) + { + glvis_command->Quit(); + } + comm_terminate: for (size_t i = 0; i < is.size(); i++) { diff --git a/lib/threads.hpp b/lib/threads.hpp index ce8cbb68..aaa8d60f 100644 --- a/lib/threads.hpp +++ b/lib/threads.hpp @@ -26,7 +26,7 @@ class GLVisCommand private: // Pointers to global GLVis data Window &win; - SdlWindow *thread_wnd; + GLWindow *thread_wnd; std::mutex glvis_mutex; std::condition_variable glvis_cond; @@ -61,7 +61,8 @@ class GLVisCommand PALETTE_REPEAT, LEVELLINES, AXIS_NUMBERFORMAT, - COLORBAR_NUMBERFORMAT + COLORBAR_NUMBERFORMAT, + QUIT }; std::atomic command_ready{false}; @@ -138,6 +139,7 @@ class GLVisCommand int ColorbarNumberFormat(std::string formatting); int Camera(const double cam[]); int Autopause(const char *mode); + int Quit(); // called by the main execution thread int Execute(); @@ -172,11 +174,15 @@ class communication_thread // signal for thread cancellation std::atomic terminate_thread {false}; + // flag for closing the window at the end of stream + bool end_quit; + static void print_commands(); void execute(); public: - communication_thread(StreamCollection _is, GLVisCommand* cmd); + communication_thread(StreamCollection _is, GLVisCommand* cmd, + bool end_quit = false); ~communication_thread(); }; diff --git a/lib/window.cpp b/lib/window.cpp index 6534d1b6..89985aaa 100644 --- a/lib/window.cpp +++ b/lib/window.cpp @@ -23,6 +23,7 @@ Window &Window::operator=(Window &&w) window_w = w.window_w; window_h = w.window_h; window_title = w.window_title; + headless = w.headless; plot_caption = std::move(w.plot_caption); extra_caption = std::move(w.extra_caption); @@ -48,8 +49,9 @@ bool Window::GLVisInitVis(StreamCollection input_streams) const char *win_title = (window_title == nullptr) ? window_titles[(int)field_type] : window_title; - internal.wnd.reset(InitVisualization(win_title, window_x, window_y, window_w, - window_h)); + GLWindow *new_wnd = InitVisualization(win_title, window_x, window_y, window_w, + window_h, headless); + if (new_wnd != wnd.get()) { internal.wnd.reset(new_wnd); } if (!wnd) { std::cerr << "Initializing the visualization failed." << std::endl; @@ -59,11 +61,14 @@ bool Window::GLVisInitVis(StreamCollection input_streams) #ifndef __EMSCRIPTEN__ if (input_streams.size() > 0) { - wnd->setOnKeyDown(SDLK_SPACE, ThreadsPauseFunc); + if (!headless) + { + wnd->setOnKeyDown(SDLK_SPACE, ThreadsPauseFunc); + } internal.glvis_command.reset(new GLVisCommand(*this)); SetGLVisCommand(glvis_command.get()); internal.comm_thread.reset(new communication_thread(std::move(input_streams), - glvis_command.get())); + glvis_command.get(), headless)); } #endif diff --git a/lib/window.hpp b/lib/window.hpp index 6793ffeb..eeb90ce1 100644 --- a/lib/window.hpp +++ b/lib/window.hpp @@ -17,7 +17,7 @@ #include "data_state.hpp" #include "stream_reader.hpp" -class SdlWindow; +class GLWindow; class VisualizationSceneScalarData; class communication_thread; class GLVisCommand; @@ -27,7 +27,7 @@ struct Window private: struct { - std::unique_ptr wnd; + std::unique_ptr wnd; std::unique_ptr vs; #ifndef __EMSCRIPTEN__ std::unique_ptr comm_thread; @@ -37,7 +37,7 @@ struct Window public: DataState data_state; - const std::unique_ptr &wnd{internal.wnd}; + const std::unique_ptr &wnd{internal.wnd}; const std::unique_ptr &vs{internal.vs}; #ifndef __EMSCRIPTEN__ const std::unique_ptr &comm_thread {internal.comm_thread}; @@ -49,6 +49,7 @@ struct Window int window_w = 400; int window_h = 350; const char *window_title = nullptr; + bool headless = false; std::string plot_caption; std::string extra_caption; diff --git a/makefile b/makefile index 1b4d5ad0..9d99f71f 100644 --- a/makefile +++ b/makefile @@ -241,6 +241,22 @@ else # no flag --> SDL screenshots endif +# EGL headless rendering +GLVIS_USE_EGL ?= NO +EGL_OPTS = -DGLVIS_USE_EGL +EGL_LIBS = -lEGL +ifeq ($(GLVIS_USE_EGL),YES) + GLVIS_FLAGS += $(EGL_OPTS) + GLVIS_LIBS += $(EGL_LIBS) +endif + +# CGL headless rendering +GLVIS_USE_CGL ?= $(if $(NOTMAC),NO,YES) +CGL_OPTS = -DGLVIS_USE_CGL +ifeq ($(GLVIS_USE_CGL),YES) + GLVIS_FLAGS += $(CGL_OPTS) +endif + PTHREAD_LIB = -lpthread GLVIS_LIBS += $(PTHREAD_LIB) @@ -248,40 +264,42 @@ LIBS = $(strip $(GLVIS_LIBS) $(GLVIS_LDFLAGS)) CCC = $(strip $(CXX) $(GLVIS_FLAGS)) Ccc = $(strip $(CC) $(CFLAGS) $(GL_OPTS)) -# generated with 'echo lib/gl/*.c* lib/*.c*', does not include lib/*.m (Obj-C) +# generated with 'echo lib/egl/*.c* lib/gl/*.c* lib/sdl/*.c* lib/*.c*', does not include lib/*.m (Obj-C) ALL_SOURCE_FILES = \ - lib/gl/renderer.cpp lib/gl/renderer_core.cpp lib/gl/renderer_ff.cpp \ - lib/gl/shader.cpp lib/gl/types.cpp lib/aux_js.cpp lib/aux_vis.cpp \ - lib/coll_reader.cpp lib/data_state.cpp lib/file_reader.cpp \ - lib/font.cpp lib/gl2ps.c lib/gltf.cpp lib/material.cpp \ - lib/openglvis.cpp lib/palettes_base.cpp lib/palettes.cpp \ - lib/palettes_default.cpp lib/script_controller.cpp lib/sdl.cpp \ - lib/sdl_helper.cpp lib/sdl_main.cpp lib/sdl_windows.cpp \ - lib/sdl_x11.cpp lib/stream_reader.cpp lib/threads.cpp lib/vsdata.cpp \ - lib/vssolution.cpp lib/vssolution3d.cpp lib/vsvector.cpp \ - lib/vsvector3d.cpp lib/window.cpp -OBJC_SOURCE_FILES = $(if $(NOTMAC),,lib/sdl_mac.mm) + lib/egl/egl.cpp lib/egl/egl_main.cpp lib/gl/renderer_core.cpp \ + lib/gl/renderer.cpp lib/gl/renderer_ff.cpp lib/gl/shader.cpp \ + lib/gl/types.cpp lib/sdl/sdl.cpp lib/sdl/sdl_helper.cpp \ + lib/sdl/sdl_main.cpp lib/sdl/sdl_windows.cpp lib/sdl/sdl_x11.cpp \ + lib/aux_js.cpp lib/aux_vis.cpp lib/coll_reader.cpp lib/data_state.cpp \ + lib/file_reader.cpp lib/font.cpp lib/gl2ps.c lib/gltf.cpp \ + lib/glwindow.cpp lib/material.cpp lib/openglvis.cpp \ + lib/palettes_base.cpp lib/palettes.cpp lib/palettes_default.cpp \ + lib/script_controller.cpp lib/stream_reader.cpp lib/threads.cpp \ + lib/vsdata.cpp lib/vssolution.cpp lib/vssolution3d.cpp \ + lib/vsvector.cpp lib/vsvector3d.cpp lib/window.cpp +OBJC_SOURCE_FILES = $(if $(NOTMAC),,lib/sdl/sdl_mac.mm) DESKTOP_ONLY_SOURCE_FILES = \ lib/gl/renderer_ff.cpp lib/threads.cpp lib/gl2ps.c \ - lib/script_controller.cpp lib/sdl_x11.cpp + lib/script_controller.cpp lib/sdl/sdl_x11.cpp WEB_ONLY_SOURCE_FILES = lib/aux_js.cpp LOGO_FILE = share/logo.rgba LOGO_FILE_CPP = $(LOGO_FILE).bin.cpp COMMON_SOURCE_FILES = $(filter-out \ $(DESKTOP_ONLY_SOURCE_FILES) $(WEB_ONLY_SOURCE_FILES),$(ALL_SOURCE_FILES)) -# generated with 'echo lib/gl/*.h* lib/*.h*' +# generated with 'echo lib/egl/*.h* lib/gl/*.h* lib/sdl/*.h* lib/*.h*' HEADER_FILES = \ - lib/gl/attr_traits.hpp lib/gl/platform_gl.hpp lib/gl/renderer.hpp \ - lib/gl/shader.hpp lib/gl/renderer_core.hpp lib/gl/renderer_ff.hpp \ - lib/gl/types.hpp lib/aux_vis.hpp lib/coll_reader.hpp \ - lib/data_state.hpp lib/file_reader.hpp lib/font.hpp \ - lib/geom_utils.hpp lib/gl2ps.h lib/gltf.hpp lib/logo.hpp \ - lib/material.hpp lib/openglvis.hpp lib/palettes_base.hpp \ - lib/palettes.hpp lib/script_controller.hpp lib/sdl_helper.hpp \ - lib/sdl.hpp lib/sdl_mac.hpp lib/sdl_main.hpp lib/sdl_windows.hpp \ - lib/sdl_x11.hpp lib/stream_reader.hpp lib/threads.hpp lib/visual.hpp \ - lib/vsdata.hpp lib/vssolution.hpp lib/vssolution3d.hpp \ + lib/egl/egl.hpp lib/egl/egl_main.hpp lib/gl/attr_traits.hpp \ + lib/gl/platform_gl.hpp lib/gl/renderer_core.hpp lib/gl/renderer_ff.hpp \ + lib/gl/renderer.hpp lib/gl/shader.hpp lib/gl/types.hpp \ + lib/sdl/sdl_helper.hpp lib/sdl/sdl.hpp lib/sdl/sdl_mac.hpp \ + lib/sdl/sdl_main.hpp lib/sdl/sdl_windows.hpp lib/sdl/sdl_x11.hpp \ + lib/aux_vis.hpp lib/coll_reader.hpp lib/data_state.hpp \ + lib/file_reader.hpp lib/font.hpp lib/geom_utils.hpp lib/gl2ps.h \ + lib/gltf.hpp lib/glwindow.hpp lib/logo.hpp lib/material.hpp \ + lib/openglvis.hpp lib/palettes_base.hpp lib/palettes.hpp \ + lib/script_controller.hpp lib/stream_reader.hpp lib/threads.hpp \ + lib/visual.hpp lib/vsdata.hpp lib/vssolution.hpp lib/vssolution3d.hpp \ lib/vsvector.hpp lib/vsvector3d.hpp lib/window.hpp DESKTOP_SOURCE_FILES = $(COMMON_SOURCE_FILES) $(DESKTOP_ONLY_SOURCE_FILES) $(LOGO_FILE_CPP) @@ -337,7 +355,7 @@ lib/libglvis.js: $(BYTECODE_FILES) $(CONFIG_MK) $(MFEM_LIB_FILE) $(EMCC) $(EMCC_OPTS) -o $@ $(BYTECODE_FILES) $(EMCC_LIBS) --embed-file $(FONT_FILE) clean: - rm -rf lib/*.o lib/*.bc lib/gl/*.o lib/gl/*.bc lib/*~ *~ glvis + rm -rf lib/*.o lib/*.bc lib/egl/*.o lib/egl/*.bc lib/gl/*.o lib/gl/*.bc lib/sdl/*.o lib/sdl/*.bc lib/*~ *~ glvis rm -rf $(LOGO_FILE_CPP) share/*.o rm -rf lib/libglvis.a lib/libglvis.js *.dSYM rm -rf GLVis.app