Skip to content

Commit

Permalink
Fixes for loading and configuration of headless EGL rendering (#307)
Browse files Browse the repository at this point in the history
* use extensions to fetch platform devices for EGL

* fix typo

* small fixes, formatter

* device preference sorting

* few more fixes

* use vector of device

* use EGLDeviceEXT

* use list of ints

* two more small fixes

* string safety and logging

* nest logging correctly

* check for extension

* missing semicolon

* more logging

* cast

* test

* test

* try fake initialize

* compile fixes

* revert debugging

* clean up logging

* cleanup

* back to front

* cleanup

* add user-facing options for headless setup

* fix framecount check

* try testing EGL on ci

* fix ci script

* fix backend string

* improve logging string formatting

* clean up context stack on shutdown

* don't check asan leaks in egl test

* use asan settings for both configurations

* comment clarity
  • Loading branch information
nmwsharp authored Dec 30, 2024
1 parent 0bcea11 commit c5bf703
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 42 deletions.
18 changes: 15 additions & 3 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ jobs:
- name: build
run: cd test/build && make

- name: run test
- name: run test mock backend
run: cd test/build && ./bin/polyscope-test --gtest_catch_exceptions=0 backend=openGL_mock

- name: run test egl backend
# We get memory leaks inside of EGL that I can't track down. With ASAN, this means the exit code is always nonzero,
# which is indistinguishable from tests failing. The ASAN_OPTIONS=detect_leaks=0 skips checking leaks for this test
# as a workaround.
run: cd test/build && ASAN_OPTIONS=detect_leaks=0 ./bin/polyscope-test --gtest_catch_exceptions=0 backend=openGL3_egl

build_shared:
strategy:
Expand All @@ -45,10 +51,16 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y xorg-dev libglu1-mesa-dev xpra xserver-xorg-video-dummy freeglut3-dev

- name: configure
run: cd test && mkdir build && cd build && cmake -DBUILD_SHARED_LIBS=TRUE -DCMAKE_BUILD_TYPE=Debug -DPOLYSCOPE_BACKEND_OPENGL3_GLFW=ON -DPOLYSCOPE_BACKEND_OPENGL_MOCK=ON ..
run: cd test && mkdir build && cd build && cmake -DBUILD_SHARED_LIBS=TRUE -DCMAKE_BUILD_TYPE=Debug -DPOLYSCOPE_BACKEND_OPENGL3_GLFW=ON -DPOLYSCOPE_BACKEND_OPENGL_MOCK=ON -DPOLYSCOPE_BACKEND_OPENGL3_EGL=ON ..

- name: build
run: cd test/build && make

- name: run test
- name: run test mock backend
run: cd test/build && ./bin/polyscope-test --gtest_catch_exceptions=0 backend=openGL_mock

- name: run test egl backend
# We get memory leaks inside of EGL that I can't track down. With ASAN, this means the exit code is always nonzero,
# which is indistinguishable from tests failing. The ASAN_OPTIONS=detect_leaks=0 skips checking leaks for this test
# as a workaround.
run: cd test/build && ASAN_OPTIONS=detect_leaks=0 ./bin/polyscope-test --gtest_catch_exceptions=0 backend=openGL3_egl
12 changes: 9 additions & 3 deletions examples/demo-app/demo_app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ int main(int argc, char** argv) {
// polyscope::options::maxFPS = -1;
polyscope::options::verbosity = 100;
polyscope::options::enableRenderErrorChecks = true;
polyscope::options::allowHeadlessBackends = true;

// Initialize polyscope
polyscope::init();
Expand All @@ -871,9 +872,14 @@ int main(int argc, char** argv) {
// Add a few gui elements
polyscope::state::userCallback = callback;

// Show the gui
polyscope::show();

if (polyscope::isHeadless()) {
// save a screenshot to prove we initialized
std::cout << "Headless mode detected, saving screenshot" << std::endl;
polyscope::screenshot("headless_screenshot.png");
} else {
// Show the gui
polyscope::show();
}
// main loop using manual frameTick() instead
// while (true) {
// polyscope::frameTick();
Expand Down
13 changes: 12 additions & 1 deletion include/polyscope/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@


namespace polyscope {
namespace options { // A general name to use when referring to the program in window headings.
namespace options {

// A general name to use when referring to the program in window headings.
extern std::string programName;

// How much should polyscope print to std::out?
Expand All @@ -28,6 +30,10 @@ extern std::string printPrefix;
// Should errors throw exceptions, or just display? (default false)
extern bool errorsThrowExceptions;

// Allow initialization to create headless backends when selecting a backend automatically
// (they can still created explicitly by name) (default: false)
extern bool allowHeadlessBackends;

// Don't let the main loop run at more than this speed. (-1 disables) (default: 60)
extern int maxFPS;

Expand Down Expand Up @@ -119,6 +125,11 @@ extern std::function<void()> configureImGuiStyleCallback;
// assign your own function to create custom styles. If this callback is null, default fonts will be used.
extern std::function<std::tuple<ImFontAtlas*, ImFont*, ImFont*>()> prepareImGuiFontsCallback;

// === Backend and low-level options

// When using the EGL backend, which device to try to initialize with
// (default is -1 which means try all of them)
extern int eglDeviceIndex;

// === Debug options

Expand Down
6 changes: 6 additions & 0 deletions include/polyscope/polyscope.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ void shutdown(bool allowMidFrameShutdown=false);
// deciding when to exit your control loop when using frameTick()
bool windowRequestsClose();

// Is Polyscope running in 'headless' mode? Headless means there is no physical display to open windows on,
// e.g. when running on a remote server. It is still possible to run Polyscope in such settings with a supported
// backend (currently, the EGL backend only), and render to save screenshots or for other purposes.
// Can only be called after initialization.
bool isHeadless();

// === Global variables ===
namespace state {

Expand Down
3 changes: 3 additions & 0 deletions include/polyscope/render/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,9 @@ class Engine {
virtual void shutdown() {};
virtual void checkError(bool fatal = false) = 0;
void buildEngineGui();

// 'headless' means there is no physical display to actually render to, e.g. when running on a remote server
virtual bool isHeadless() { return false; }

virtual void clearDisplay();
virtual void bindDisplay();
Expand Down
7 changes: 7 additions & 0 deletions include/polyscope/render/opengl/gl_engine_egl.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "glad/glad.h"
// glad must come first
#include <EGL/egl.h>
#include <EGL/eglext.h>
#endif


Expand Down Expand Up @@ -41,6 +42,9 @@ class GLEngineEGL : public GLEngine {
virtual void shutdown() override;
void swapDisplayBuffers() override;
void checkError(bool fatal = false) override;

// EGL backend is always headless
virtual bool isHeadless() override { return true; }

// === Windowing and framework things

Expand Down Expand Up @@ -74,6 +78,9 @@ class GLEngineEGL : public GLEngine {
// Internal windowing and engine details
EGLDisplay eglDisplay;
EGLContext eglContext;

// helpers
void sortAvailableDevicesByPreference(std::vector<int32_t>& deviceInds, EGLDeviceEXT rawDevices[]);
};

} // namespace backend_openGL3
Expand Down
3 changes: 3 additions & 0 deletions src/options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace options {
std::string programName = "Polyscope";
int verbosity = 2;
std::string printPrefix = "[polyscope] ";
bool allowHeadlessBackends = false;
bool errorsThrowExceptions = false;
bool debugDrawPickBuffer = false;
int maxFPS = 60;
Expand Down Expand Up @@ -55,6 +56,8 @@ bool openImGuiWindowForUserCallback = true;
std::function<void()> configureImGuiStyleCallback = configureImGuiStyle;
std::function<std::tuple<ImFontAtlas*, ImFont*, ImFont*>()> prepareImGuiFontsCallback = prepareImGuiFonts;

// Backend and low-level options
int eglDeviceIndex = -1; // means "try all of them"

// enabled by default in debug mode
#ifndef NDEBUG
Expand Down
19 changes: 19 additions & 0 deletions src/polyscope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,14 @@ void show(size_t forFrames) {
if (!state::initialized) {
exception("must initialize Polyscope with polyscope::init() before calling polyscope::show().");
}

if (isHeadless() && forFrames == 0) {
info("You called show() while in headless mode. In headless mode there is no display to create windows on. By "
"default, the show() call will block indefinitely. If you did not mean to run in headless mode, check the "
"initialization settings. Otherwise, be sure to set a callback to make something happen while polyscope is "
"showing the UI, or use functions like screenshot() to render directly without calling show().");
}

unshowRequested = false;

// the popContext() doesn't quit until _after_ the last frame, so we need to decrement by 1 to get the count right
Expand Down Expand Up @@ -934,6 +942,16 @@ bool windowRequestsClose() {
return false;
}

bool isHeadless() {
if (!isInitialized()) {
exception("must initialize Polyscope with init() before calling isHeadless().");
}
if (render::engine) {
return render::engine->isHeadless();
}
return false;
}

void shutdown(bool allowMidFrameShutdown) {

if (!allowMidFrameShutdown && contextStack.size() > 1) {
Expand All @@ -955,6 +973,7 @@ void shutdown(bool allowMidFrameShutdown) {
// Shut down the render engine
render::engine->shutdown();
delete render::engine;
contextStack.clear();
render::engine = nullptr;
state::backend = "";
state::initialized = false;
Expand Down
48 changes: 28 additions & 20 deletions src/render/initialize_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ void initializeRenderEngine(std::string backend) {
// Attempt to automatically initialize by trynig

bool initSucces = false;
std::string extraMessage = "";

#ifdef POLYSCOPE_BACKEND_OPENGL3_GLFW_ENABLED
// First try GLFW, if available
Expand All @@ -55,40 +56,47 @@ void initializeRenderEngine(std::string backend) {
initSucces = true;
} catch (const std::exception& e) {
if (options::verbosity > 0) {
info("Attempting automatic initialization. Could not initialize backend [openGL3_glfw]. Message: " +
std::string(e.what()));
info("Automatic initialization status: could not initialize backend [openGL3_glfw].");
}
}
if (initSucces) return;
#endif

#ifdef POLYSCOPE_BACKEND_OPENGL3_EGL_ENABLED
// Then, try EGL if available
engineBackendName = "openGL3_egl";
try {
backend_openGL3::initializeRenderEngine_egl();
initSucces = true;
} catch (const std::exception& e) {
if (options::verbosity > 0) {
info("Attempting automatic initialization. Could not initialize backend [openGL3_egl]. Message: " +
std::string(e.what()));

if (options::allowHeadlessBackends) {

// Then, try EGL if available
engineBackendName = "openGL3_egl";
try {
backend_openGL3::initializeRenderEngine_egl();
initSucces = true;
} catch (const std::exception& e) {
if (options::verbosity > 0) {
info("Automatic initialization status: could not initialize backend [openGL3_egl].");
}
}
}
if (initSucces) {
if (options::verbosity > 0) {
info("Automatic initialization could not create an interactive backend, and created a headless backend "
"instead. This likely means no displays are available. With the headless backend, you can still run "
"Polyscope and even render, for instance to record screenshots. However no interactive windows can be "
"created.");
if (initSucces) {
if (options::verbosity > 0) {
info("Automatic initialization could not create an interactive backend, and created a headless backend "
"instead. This likely means no displays are available. With the headless backend, you can still run "
"Polyscope and even render, for instance to save images of visualizations. However no interactive "
"windows can be created.");
}
return;
}
return;

} else {
extraMessage = " The headless EGL backend was available, but allowHeadlessBackends=false. Set it to true for "
"headless initialization.";
}

#endif

// Don't bother trying the 'mock' backend, it is unlikely to be what the user wants from the 'auto' option

// Failure
exception("Automatic initialization: no Polyscope backends could be initialized successfully.");
exception("Automatic initialization: no Polyscope backends could be initialized successfully." + extraMessage);

} else {
exception("unrecognized Polyscope backend " + backend);
Expand Down
Loading

0 comments on commit c5bf703

Please sign in to comment.