Skip to content

Commit c5bf703

Browse files
authored
Fixes for loading and configuration of headless EGL rendering (#307)
* 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
1 parent 0bcea11 commit c5bf703

File tree

12 files changed

+260
-42
lines changed

12 files changed

+260
-42
lines changed

.github/workflows/linux.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@ jobs:
2727
- name: build
2828
run: cd test/build && make
2929

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

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

4753
- name: configure
48-
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 ..
54+
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 ..
4955

5056
- name: build
5157
run: cd test/build && make
5258

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

examples/demo-app/demo_app.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,7 @@ int main(int argc, char** argv) {
846846
// polyscope::options::maxFPS = -1;
847847
polyscope::options::verbosity = 100;
848848
polyscope::options::enableRenderErrorChecks = true;
849+
polyscope::options::allowHeadlessBackends = true;
849850

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

874-
// Show the gui
875-
polyscope::show();
876-
875+
if (polyscope::isHeadless()) {
876+
// save a screenshot to prove we initialized
877+
std::cout << "Headless mode detected, saving screenshot" << std::endl;
878+
polyscope::screenshot("headless_screenshot.png");
879+
} else {
880+
// Show the gui
881+
polyscope::show();
882+
}
877883
// main loop using manual frameTick() instead
878884
// while (true) {
879885
// polyscope::frameTick();

include/polyscope/options.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414

1515
namespace polyscope {
16-
namespace options { // A general name to use when referring to the program in window headings.
16+
namespace options {
17+
18+
// A general name to use when referring to the program in window headings.
1719
extern std::string programName;
1820

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

33+
// Allow initialization to create headless backends when selecting a backend automatically
34+
// (they can still created explicitly by name) (default: false)
35+
extern bool allowHeadlessBackends;
36+
3137
// Don't let the main loop run at more than this speed. (-1 disables) (default: 60)
3238
extern int maxFPS;
3339

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

128+
// === Backend and low-level options
129+
130+
// When using the EGL backend, which device to try to initialize with
131+
// (default is -1 which means try all of them)
132+
extern int eglDeviceIndex;
122133

123134
// === Debug options
124135

include/polyscope/polyscope.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ void shutdown(bool allowMidFrameShutdown=false);
6666
// deciding when to exit your control loop when using frameTick()
6767
bool windowRequestsClose();
6868

69+
// Is Polyscope running in 'headless' mode? Headless means there is no physical display to open windows on,
70+
// e.g. when running on a remote server. It is still possible to run Polyscope in such settings with a supported
71+
// backend (currently, the EGL backend only), and render to save screenshots or for other purposes.
72+
// Can only be called after initialization.
73+
bool isHeadless();
74+
6975
// === Global variables ===
7076
namespace state {
7177

include/polyscope/render/engine.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,9 @@ class Engine {
445445
virtual void shutdown() {};
446446
virtual void checkError(bool fatal = false) = 0;
447447
void buildEngineGui();
448+
449+
// 'headless' means there is no physical display to actually render to, e.g. when running on a remote server
450+
virtual bool isHeadless() { return false; }
448451

449452
virtual void clearDisplay();
450453
virtual void bindDisplay();

include/polyscope/render/opengl/gl_engine_egl.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "glad/glad.h"
1313
// glad must come first
1414
#include <EGL/egl.h>
15+
#include <EGL/eglext.h>
1516
#endif
1617

1718

@@ -41,6 +42,9 @@ class GLEngineEGL : public GLEngine {
4142
virtual void shutdown() override;
4243
void swapDisplayBuffers() override;
4344
void checkError(bool fatal = false) override;
45+
46+
// EGL backend is always headless
47+
virtual bool isHeadless() override { return true; }
4448

4549
// === Windowing and framework things
4650

@@ -74,6 +78,9 @@ class GLEngineEGL : public GLEngine {
7478
// Internal windowing and engine details
7579
EGLDisplay eglDisplay;
7680
EGLContext eglContext;
81+
82+
// helpers
83+
void sortAvailableDevicesByPreference(std::vector<int32_t>& deviceInds, EGLDeviceEXT rawDevices[]);
7784
};
7885

7986
} // namespace backend_openGL3

src/options.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace options {
99
std::string programName = "Polyscope";
1010
int verbosity = 2;
1111
std::string printPrefix = "[polyscope] ";
12+
bool allowHeadlessBackends = false;
1213
bool errorsThrowExceptions = false;
1314
bool debugDrawPickBuffer = false;
1415
int maxFPS = 60;
@@ -55,6 +56,8 @@ bool openImGuiWindowForUserCallback = true;
5556
std::function<void()> configureImGuiStyleCallback = configureImGuiStyle;
5657
std::function<std::tuple<ImFontAtlas*, ImFont*, ImFont*>()> prepareImGuiFontsCallback = prepareImGuiFonts;
5758

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

5962
// enabled by default in debug mode
6063
#ifndef NDEBUG

src/polyscope.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,14 @@ void show(size_t forFrames) {
893893
if (!state::initialized) {
894894
exception("must initialize Polyscope with polyscope::init() before calling polyscope::show().");
895895
}
896+
897+
if (isHeadless() && forFrames == 0) {
898+
info("You called show() while in headless mode. In headless mode there is no display to create windows on. By "
899+
"default, the show() call will block indefinitely. If you did not mean to run in headless mode, check the "
900+
"initialization settings. Otherwise, be sure to set a callback to make something happen while polyscope is "
901+
"showing the UI, or use functions like screenshot() to render directly without calling show().");
902+
}
903+
896904
unshowRequested = false;
897905

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

945+
bool isHeadless() {
946+
if (!isInitialized()) {
947+
exception("must initialize Polyscope with init() before calling isHeadless().");
948+
}
949+
if (render::engine) {
950+
return render::engine->isHeadless();
951+
}
952+
return false;
953+
}
954+
937955
void shutdown(bool allowMidFrameShutdown) {
938956

939957
if (!allowMidFrameShutdown && contextStack.size() > 1) {
@@ -955,6 +973,7 @@ void shutdown(bool allowMidFrameShutdown) {
955973
// Shut down the render engine
956974
render::engine->shutdown();
957975
delete render::engine;
976+
contextStack.clear();
958977
render::engine = nullptr;
959978
state::backend = "";
960979
state::initialized = false;

src/render/initialize_backend.cpp

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ void initializeRenderEngine(std::string backend) {
4646
// Attempt to automatically initialize by trynig
4747

4848
bool initSucces = false;
49+
std::string extraMessage = "";
4950

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

6565
#ifdef POLYSCOPE_BACKEND_OPENGL3_EGL_ENABLED
66-
// Then, try EGL if available
67-
engineBackendName = "openGL3_egl";
68-
try {
69-
backend_openGL3::initializeRenderEngine_egl();
70-
initSucces = true;
71-
} catch (const std::exception& e) {
72-
if (options::verbosity > 0) {
73-
info("Attempting automatic initialization. Could not initialize backend [openGL3_egl]. Message: " +
74-
std::string(e.what()));
66+
67+
if (options::allowHeadlessBackends) {
68+
69+
// Then, try EGL if available
70+
engineBackendName = "openGL3_egl";
71+
try {
72+
backend_openGL3::initializeRenderEngine_egl();
73+
initSucces = true;
74+
} catch (const std::exception& e) {
75+
if (options::verbosity > 0) {
76+
info("Automatic initialization status: could not initialize backend [openGL3_egl].");
77+
}
7578
}
76-
}
77-
if (initSucces) {
78-
if (options::verbosity > 0) {
79-
info("Automatic initialization could not create an interactive backend, and created a headless backend "
80-
"instead. This likely means no displays are available. With the headless backend, you can still run "
81-
"Polyscope and even render, for instance to record screenshots. However no interactive windows can be "
82-
"created.");
79+
if (initSucces) {
80+
if (options::verbosity > 0) {
81+
info("Automatic initialization could not create an interactive backend, and created a headless backend "
82+
"instead. This likely means no displays are available. With the headless backend, you can still run "
83+
"Polyscope and even render, for instance to save images of visualizations. However no interactive "
84+
"windows can be created.");
85+
}
86+
return;
8387
}
84-
return;
88+
89+
} else {
90+
extraMessage = " The headless EGL backend was available, but allowHeadlessBackends=false. Set it to true for "
91+
"headless initialization.";
8592
}
93+
8694
#endif
8795

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

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

93101
} else {
94102
exception("unrecognized Polyscope backend " + backend);

0 commit comments

Comments
 (0)