From f833e295bdf6367fa685a348cee1917a74584156 Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Tue, 22 Sep 2026 08:16:38 +0900 Subject: [PATCH 1/7] Fix runSofaGLFW crashing on every scene (missing init, refresh registering object and camera missing) --- SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp | 13 ++++++------- exe/Main.cpp | 21 ++++++++++++++++++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp index 9e7452481a..6df20deedd 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp @@ -69,6 +69,12 @@ void SofaGLFWWindow::close() void SofaGLFWWindow::draw(simulation::NodeSPtr groot, core::visual::VisualParams* vparams) { + if (!m_currentCamera) + { + msg_error("SofaGLFWGUI") << "No camera defined."; + return; + } + glClearColor(m_backgroundColor.r(), m_backgroundColor.g(), m_backgroundColor.b(), m_backgroundColor.a()); glClearDepth(1.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -81,13 +87,6 @@ void SofaGLFWWindow::draw(simulation::NodeSPtr groot, core::visual::VisualParams glDisable(GL_COLOR_MATERIAL); // draw the scene - if (!m_currentCamera) - { - msg_error("SofaGLFWGUI") << "No camera defined."; - return; - } - - if (groot->f_bbox.getValue().isValid()) { vparams->sceneBBox() = groot->f_bbox.getValue(); diff --git a/exe/Main.cpp b/exe/Main.cpp index 40e080fca7..504006bdc3 100644 --- a/exe/Main.cpp +++ b/exe/Main.cpp @@ -28,8 +28,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -66,6 +68,8 @@ int main(int argc, char** argv) sofa::helper::BackTrace::autodump(); + sofa::simulation::common::init(); + // create an instance of SofaGLFWGUI // linked with the simulation sofaglfw::SofaGLFWBaseGUI glfwGUI; @@ -78,9 +82,19 @@ int main(int argc, char** argv) return 0; } + auto& pluginManager = sofa::helper::system::PluginManager::getInstance(); + for (const auto& plugin : pluginsToLoad) { - sofa::helper::system::PluginManager::getInstance().loadPlugin(plugin); + pluginManager.loadPlugin(plugin); + } + + pluginManager.init(); + + sofa::core::ObjectFactory* objectFactory = sofa::core::ObjectFactory::getInstance(); + for (const auto& [pluginPath, plugin] : pluginManager.getPluginMap()) + { + objectFactory->registerObjectsFromPlugin(plugin.getModuleName()); } std::string fileName = result["file"].as(); @@ -96,6 +110,9 @@ int main(int argc, char** argv) glfwGUI.setSimulation(groot, fileName); + // create camera, visual style, pick handler ... + glfwGUI.load(); + bool isFullScreen = result["fullscreen"].as(); sofa::type::Vec2i resolution{ 800, 600}; sofa::component::setting::ViewerSetting* viewerConf; @@ -157,5 +174,7 @@ int main(int argc, char** argv) sofa::simulation::node::unload(groot); } + sofa::simulation::common::cleanup(); + return 0; } From 1fc9b2241d0e23307d84e8fea005c68e93b9431e Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Tue, 22 Sep 2026 08:28:24 +0900 Subject: [PATCH 2/7] misc fixes ogl attributes, viewer settings and config file --- README.md | 2 ++ SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp | 9 +++++---- exe/CMakeLists.txt | 4 +++- exe/Main.cpp | 10 ++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 962d357017..35e42a535c 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ Using the SHIFT + CTRL keys, you will trigger events on key pressed/released. * `-a` or `--start`: if true, starts the simulation just after opening. True by default. * `-s` or `--fullscreen`: set full screen at startup. False by default. * `-l` or `--load`: load given plugins as a comma-separated list. Example: -l SofaPython3 +* `-m` or `--msaa_samples`: set the number of samples for multisample anti-aliasing (MSAA). 0 by default. +* `-n` or `--nb_iterations`: set the number of iterations to run (batch mode). 0 (unlimited) by default. ## Dear ImGui diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp index 6df20deedd..033a1f77be 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp @@ -168,14 +168,15 @@ void SofaGLFWWindow::drawBackgroundImage() if(!m_backgrounds.contains(m_currentBackgroundFilename)) return; - glPushAttrib(GL_ALL_ATTRIB_BITS); - - glDisable(GL_LIGHTING); - const auto& background = m_backgrounds[m_currentBackgroundFilename]; + // before glPushAttrib(), otherwise the attribute stack leaks on every frame if(!background.image) return; + + glPushAttrib(GL_ALL_ATTRIB_BITS); + + glDisable(GL_LIGHTING); const int imageWidth = background.image->getWidth(); const int imageHeight = background.image->getHeight(); diff --git a/exe/CMakeLists.txt b/exe/CMakeLists.txt index 2b7ead3904..8685e13f5e 100644 --- a/exe/CMakeLists.txt +++ b/exe/CMakeLists.txt @@ -13,12 +13,14 @@ if(NOT TARGET Sofa.Helper) # temporary fix about aliases problem sofa_find_package(Sofa.Simulation.Core REQUIRED) endif() +sofa_find_package(Sofa.GUI.Common REQUIRED) + set(SOURCE_FILES Main.cpp) add_executable(${PROJECT_NAME} ${SOURCE_FILES}) -target_link_libraries(${PROJECT_NAME} Sofa.Simulation.Core SofaGLFW) +target_link_libraries(${PROJECT_NAME} Sofa.Simulation.Core Sofa.GUI.Common SofaGLFW) target_include_directories(${PROJECT_NAME} PRIVATE extlibs/cxxopts-2.2.1/include) sofa_add_targets_to_package( diff --git a/exe/Main.cpp b/exe/Main.cpp index 504006bdc3..cb1566ac4a 100644 --- a/exe/Main.cpp +++ b/exe/Main.cpp @@ -35,8 +35,11 @@ #include #include #include +#include +#include #include +#include #include @@ -70,6 +73,10 @@ int main(int argc, char** argv) sofa::simulation::common::init(); + // same configuration directory as runSofa + sofa::gui::common::BaseGUI::setConfigDirectoryPath( + sofa::helper::system::FileSystem::append(sofa::helper::Utils::getSofaUserLocalDirectory(), "config"), true); + // create an instance of SofaGLFWGUI // linked with the simulation sofaglfw::SofaGLFWBaseGUI glfwGUI; @@ -146,6 +153,9 @@ int main(int argc, char** argv) glfwGUI.initVisual(); + // camera of the '.view' sidecar file, if any + glfwGUI.restoreCamera(glfwGUI.getCamera()); + //Background sofa::component::setting::BackgroundSetting* background; groot->get(background, sofa::core::objectmodel::BaseContext::SearchRoot); From 266dec16d3f2f25964604fec103be65ee1b21d0e Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Tue, 22 Sep 2026 10:35:23 +0900 Subject: [PATCH 3/7] Add an option to render offscreen Render without showing anything on screen, while still calling all the graphics functions: the window and its OpenGL context are created as usual, they are simply never mapped on screen (GLFW_VISIBLE hint). Available as '--offscreen' in runSofaGLFW, and in runSofa with the glfw and imgui GUIs. SofaGLFWGUI::RegisterGUIParameters() declares it for both of them: GUIManager declares the parameters of every registered GUI, so the function guards against a second registration. Fullscreen is ignored in offscreen mode, and switchFullScreen() (F11) is refused, since it would map the window. --- README.md | 8 ++++++ SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp | 16 +++++++++++ SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h | 7 +++++ SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp | 34 +++++++++++++++++++++++ SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h | 14 ++++++++++ SofaGLFW/src/SofaGLFW/initSofaGLFW.cpp | 2 +- SofaImGui/src/SofaImGui/initSofaImGui.cpp | 2 +- exe/Main.cpp | 9 ++++++ 8 files changed, 90 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 35e42a535c..79d0ed0aef 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,14 @@ Using the SHIFT + CTRL keys, you will trigger events on key pressed/released. * `-l` or `--load`: load given plugins as a comma-separated list. Example: -l SofaPython3 * `-m` or `--msaa_samples`: set the number of samples for multisample anti-aliasing (MSAA). 0 by default. * `-n` or `--nb_iterations`: set the number of iterations to run (batch mode). 0 (unlimited) by default. +* `--offscreen`: render offscreen. No window is shown, but the graphics functions are still called. False by default. + The window and its OpenGL context are created as usual, they are simply never mapped on screen. Combine it with + `--nb_iterations` to get a finite run, since there is no window to close. + +The `--offscreen` option is also available from `runSofa` with the `glfw` and `imgui` GUIs: +`runSofa -g imgui -l SofaImGui --offscreen myScene.scn` +(as for any GUI argument coming from a plugin, it is only listed by `runSofa --help` when the plugin is autoloaded, +but it is taken into account in all cases). ## Dear ImGui diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp index 950ddfcaee..c77842ef19 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp @@ -251,6 +251,15 @@ bool SofaGLFWBaseGUI::createWindow(int width, int height, const char* title, boo return false; } + // the hint is sticky, hence always set explicitly + glfwWindowHint(GLFW_VISIBLE, m_bOffscreen ? GLFW_FALSE : GLFW_TRUE); + + if (m_bOffscreen && fullscreenAtStartup) + { + msg_warning("SofaGLFWBaseGUI") << "Fullscreen is ignored in offscreen mode."; + fullscreenAtStartup = false; + } + GLFWwindow* glfwWindow = nullptr; if (fullscreenAtStartup) { @@ -275,6 +284,7 @@ bool SofaGLFWBaseGUI::createWindow(int width, int height, const char* title, boo s_numberOfActiveWindows++; #ifndef __APPLE__ // Apple implies Cocoa and Cocoa does not support icon for the window + if (!m_bOffscreen) { setWindowIcon(glfwWindow); } @@ -383,6 +393,12 @@ bool SofaGLFWBaseGUI::isFullScreen(GLFWwindow* glfwWindow) const void SofaGLFWBaseGUI::switchFullScreen(GLFWwindow* glfwWindow, unsigned int /* screenID */) { + if (m_bOffscreen) + { + msg_warning("SofaGLFWBaseGUI") << "Cannot switch to fullscreen in offscreen mode."; + return; + } + if (hasWindow()) { // only manage the first window for now diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h index da5d53876b..6ab75d504a 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h @@ -57,6 +57,12 @@ class SOFAGLFW_API SofaGLFWBaseGUI : public BaseViewer bool createWindow(int width, int height, const char* title, bool fullscreenAtStartup = false); void destroyWindow(); + + /// Window and OpenGL context created but never shown; graphics functions are still + /// called. To be set before createWindow(). + void setOffscreen(bool offscreen) { m_bOffscreen = offscreen; } + bool isOffscreen() const { return m_bOffscreen; } + void initVisual(); std::size_t runLoop(std::size_t targetNbIterations = 0); void terminate(); @@ -156,6 +162,7 @@ class SOFAGLFW_API SofaGLFWBaseGUI : public BaseViewer bool m_bGlfwIsInitialized{ false }; bool m_bGlewIsInitialized{ false }; + bool m_bOffscreen{ false }; std::unique_ptr m_glDrawTool; sofa::core::visual::VisualParams* m_vparams{ nullptr }; diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp index fb959267b1..dc9c81a5b2 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp @@ -25,12 +25,15 @@ #include #include #include +#include using namespace sofa; namespace sofaglfw { +sofa::gui::common::ArgumentParser* SofaGLFWGUI::s_argumentParser = nullptr; + bool SofaGLFWGUI::init() { return m_baseGUI.init(); @@ -42,6 +45,16 @@ int SofaGLFWGUI::mainLoop() return 0; } +bool SofaGLFWGUI::isOffscreenRequested() +{ + bool offscreen = false; + if (s_argumentParser) + { + s_argumentParser->getValueFromKey("offscreen", offscreen); + } + return offscreen; +} + void SofaGLFWGUI::redraw() { @@ -63,6 +76,7 @@ void SofaGLFWGUI::setScene(sofa::simulation::NodeSPtr groot, const char* filenam m_baseGUI.setSimulation(groot, strFilename); m_baseGUI.load(); + m_baseGUI.setOffscreen(isOffscreenRequested()); m_baseGUI.createWindow(m_baseGUI.getWindowWidth(), m_baseGUI.getWindowHeight(), std::string("SOFA - " + strFilename).c_str(), m_bCreateWithFullScreen); // needs to be done after for background @@ -140,6 +154,26 @@ sofa::gui::common::BaseGUI* SofaGLFWGUI::CreateGUI(const char* name, sofa::simul return gui; } +int SofaGLFWGUI::RegisterGUIParameters(sofa::gui::common::ArgumentParser* argumentParser) +{ + s_argumentParser = argumentParser; + + // GUIManager declares the parameters of every registered GUI: only add them once + static bool alreadyRegistered = false; + if (alreadyRegistered) + { + return 0; + } + alreadyRegistered = true; + + argumentParser->addArgument( + cxxopts::value()->default_value("false"), + "offscreen", + "(only glfw/imgui) render offscreen: no window is shown but the graphics functions are still called" + ); + return 0; +} + void SofaGLFWGUI::setMouseButtonConfiguration(sofa::component::setting::MouseButtonSetting *setting) { if (setting) diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h index 0037d5f0a3..194b8512a9 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h @@ -27,6 +27,11 @@ #include +namespace sofa::gui::common +{ + class ArgumentParser; +} + namespace sofaglfw { @@ -52,12 +57,21 @@ class SOFAGLFW_API SofaGLFWGUI : public sofa::gui::common::BaseGUI void setBackgroundColor(const sofa::type::RGBAColor& color) override; void setBackgroundImage(const std::string& image) override; static sofa::gui::common::BaseGUI * CreateGUI(const char* name, sofa::simulation::NodeSPtr groot, const char* filename); + /// Command line arguments shared by the GLFW-based GUIs (glfw, imgui); calling it more + /// than once is a no-op. + static int RegisterGUIParameters(sofa::gui::common::ArgumentParser* argumentParser); + + /// Whether '--offscreen' was given on the command line + static bool isOffscreenRequested(); void setMouseButtonConfiguration(sofa::component::setting::MouseButtonSetting *setting) override; virtual BaseViewer* getViewer() override; protected: SofaGLFWBaseGUI m_baseGUI; bool m_bCreateWithFullScreen{ false }; + + /// duplicates GUIManager::currentArgumentParser, protected: would need an accessor in SOFA + static sofa::gui::common::ArgumentParser* s_argumentParser; }; } // namespace sofaglfw diff --git a/SofaGLFW/src/SofaGLFW/initSofaGLFW.cpp b/SofaGLFW/src/SofaGLFW/initSofaGLFW.cpp index bf184b5cbb..dcbcd4dd37 100644 --- a/SofaGLFW/src/SofaGLFW/initSofaGLFW.cpp +++ b/SofaGLFW/src/SofaGLFW/initSofaGLFW.cpp @@ -47,7 +47,7 @@ void init() { first = false; #if SOFAGLFW_HAVE_SOFA_GUI_COMMON - sofa::gui::common::GUIManager::RegisterGUI("glfw", &sofaglfw::SofaGLFWGUI::CreateGUI,nullptr,0); + sofa::gui::common::GUIManager::RegisterGUI("glfw", &sofaglfw::SofaGLFWGUI::CreateGUI, &sofaglfw::SofaGLFWGUI::RegisterGUIParameters, 0); #endif // SOFAGLFW_HAVE_SOFA_GUI_COMMON } } diff --git a/SofaImGui/src/SofaImGui/initSofaImGui.cpp b/SofaImGui/src/SofaImGui/initSofaImGui.cpp index 4bdba6a27b..9c2568c5c8 100644 --- a/SofaImGui/src/SofaImGui/initSofaImGui.cpp +++ b/SofaImGui/src/SofaImGui/initSofaImGui.cpp @@ -48,7 +48,7 @@ void init() sofa::helper::logging::MessageDispatcher::addHandler(&sofa::helper::logging::MainLoggingMessageHandler::getInstance()); sofa::helper::logging::MainLoggingMessageHandler::getInstance().activate(); - sofa::gui::common::GUIManager::RegisterGUI("imgui", &sofaimgui::ImGuiGUI::CreateGUI,nullptr,1); + sofa::gui::common::GUIManager::RegisterGUI("imgui", &sofaimgui::ImGuiGUI::CreateGUI, &sofaglfw::SofaGLFWGUI::RegisterGUIParameters, 1); } } diff --git a/exe/Main.cpp b/exe/Main.cpp index cb1566ac4a..27968f951b 100644 --- a/exe/Main.cpp +++ b/exe/Main.cpp @@ -55,6 +55,7 @@ int main(int argc, char** argv) ("l,load", "load given plugins as a comma-separated list. Example: -l SofaPython3", cxxopts::value >(pluginsToLoad)) ("m,msaa_samples", "set number of samples for multisample anti-aliasing (MSAA)", cxxopts::value()->default_value("0")) ("n,nb_iterations", "set number of iterations to run (batch mode)", cxxopts::value()->default_value("0")) + ("offscreen", "render offscreen: no window is shown but the graphics functions are still called", cxxopts::value()->default_value("false")) ("h,help", "print usage") ; @@ -136,6 +137,9 @@ int main(int argc, char** argv) } } + const bool isOffscreen = result["offscreen"].as(); + glfwGUI.setOffscreen(isOffscreen); + // create a SofaGLFW window glfwGUI.createWindow(resolution[0], resolution[1], "SofaGLFW", isFullScreen); @@ -147,6 +151,11 @@ int main(int argc, char** argv) msg_info("SofaGLFW") << "Batch mode: computing " << targetNbIterations << " iterations."; startAnim = true; } + else if (isOffscreen) + { + msg_warning("SofaGLFW") << "Offscreen mode without a number of iterations (--nb_iterations): " + "the simulation will run until the process is interrupted."; + } if (startAnim) groot->setAnimate(true); From 1d1a71e716a1f870e65a82191120f23db4139d3e Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Tue, 22 Sep 2026 10:35:52 +0900 Subject: [PATCH 4/7] Honour the number of iterations in the glfw and imgui GUIs '-n'/'--nbIter' only stopped the batch GUI; with glfw or imgui the simulation ran until the window was closed, which made it impossible to run a bounded simulation while still rendering, e.g. together with '--offscreen'. The argument is registered by the batch GUI, and cxxopts refuses a duplicate, so it is not declared a second time: SofaGLFWGUI keeps the ArgumentParser given to RegisterGUIParameters and reads the already parsed value in mainLoop(). Only arguments actually present on the command line end up in the parse result, hence the simulation keeps running indefinitely when '-n' is not given. 'infinite' is supported as in the batch GUI. --- README.md | 4 ++++ SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp | 28 ++++++++++++++++++++++++++- SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h | 4 ++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 79d0ed0aef..40ad8fcded 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ Using the SHIFT + CTRL keys, you will trigger events on key pressed/released. The window and its OpenGL context are created as usual, they are simply never mapped on screen. Combine it with `--nb_iterations` to get a finite run, since there is no window to close. +The `-n`/`--nbIter` option of the batch GUI is also honoured by the `glfw` and `imgui` GUIs: +`runSofa -g imgui -l SofaImGui -n 100 myScene.scn` stops after 100 iterations, and `infinite` +(the batch default) keeps running. + The `--offscreen` option is also available from `runSofa` with the `glfw` and `imgui` GUIs: `runSofa -g imgui -l SofaImGui --offscreen myScene.scn` (as for any GUI argument coming from a plugin, it is only listed by `runSofa --help` when the plugin is autoloaded, diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp index dc9c81a5b2..9da26ae747 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp @@ -26,6 +26,7 @@ #include #include #include +#include using namespace sofa; @@ -41,7 +42,7 @@ bool SofaGLFWGUI::init() int SofaGLFWGUI::mainLoop() { - m_baseGUI.runLoop(); + m_baseGUI.runLoop(getTargetNbIterations()); return 0; } @@ -55,6 +56,31 @@ bool SofaGLFWGUI::isOffscreenRequested() return offscreen; } +std::size_t SofaGLFWGUI::getTargetNbIterations() +{ + // '-n'/'--nbIter' is registered by the batch GUI; it is only read here + std::string value; + if (!s_argumentParser || !s_argumentParser->getValueFromKey("nbIter", value)) + { + return 0; + } + + if (value == "infinite") + { + return 0; + } + + int nbIterations = 0; + if (!sofa::type::hardening::safeStrToInt(value, nbIterations) || nbIterations < 0) + { + msg_warning("SofaGLFWGUI") << "Invalid number of iterations: '" << value << "'. Ignored."; + return 0; + } + + msg_info("SofaGLFWGUI") << "Computing " << nbIterations << " iterations."; + return static_cast(nbIterations); +} + void SofaGLFWGUI::redraw() { diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h index 194b8512a9..d0ec5bf3f3 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h @@ -61,6 +61,10 @@ class SOFAGLFW_API SofaGLFWGUI : public sofa::gui::common::BaseGUI /// than once is a no-op. static int RegisterGUIParameters(sofa::gui::common::ArgumentParser* argumentParser); + /// Number of iterations requested with the '-n'/'--nbIter' argument of the batch GUI; + /// 0 when unlimited. + static std::size_t getTargetNbIterations(); + /// Whether '--offscreen' was given on the command line static bool isOffscreenRequested(); void setMouseButtonConfiguration(sofa::component::setting::MouseButtonSetting *setting) override; From 1093333e7d9843bacadd7b2dce795f24668b3085 Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Tue, 22 Sep 2026 10:36:15 +0900 Subject: [PATCH 5/7] Ignore the offscreen option in the imgui GUI The imgui GUI is the glfw one plus interactive widgets, so rendering it without ever showing the window serves no purpose. --- README.md | 7 +++++-- SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp | 10 ++++++++-- SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h | 3 +++ SofaImGui/src/SofaImGui/ImGuiGUI.h | 3 +++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 40ad8fcded..869fde1270 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,14 @@ The `-n`/`--nbIter` option of the batch GUI is also honoured by the `glfw` and ` `runSofa -g imgui -l SofaImGui -n 100 myScene.scn` stops after 100 iterations, and `infinite` (the batch default) keeps running. -The `--offscreen` option is also available from `runSofa` with the `glfw` and `imgui` GUIs: -`runSofa -g imgui -l SofaImGui --offscreen myScene.scn` +The `--offscreen` option is also available from `runSofa` with the `glfw` GUI: +`runSofa -g glfw -l SofaGLFW --offscreen myScene.scn` (as for any GUI argument coming from a plugin, it is only listed by `runSofa --help` when the plugin is autoloaded, but it is taken into account in all cases). +It is ignored, with a warning, by the `imgui` GUI: that GUI is the `glfw` one plus interactive widgets, so +rendering it offscreen would serve no purpose. + ## Dear ImGui By default, SofaGLFW does not show any user interface. diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp index 9da26ae747..3981fbc53d 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp @@ -102,7 +102,13 @@ void SofaGLFWGUI::setScene(sofa::simulation::NodeSPtr groot, const char* filenam m_baseGUI.setSimulation(groot, strFilename); m_baseGUI.load(); - m_baseGUI.setOffscreen(isOffscreenRequested()); + const bool interactive = isInteractive(); + const bool offscreen = isOffscreenRequested(); + if (interactive && offscreen) + { + msg_warning("SofaGLFWGUI") << "The '" << mGuiName << "' GUI is interactive: '--offscreen' is ignored."; + } + m_baseGUI.setOffscreen(!interactive && offscreen); m_baseGUI.createWindow(m_baseGUI.getWindowWidth(), m_baseGUI.getWindowHeight(), std::string("SOFA - " + strFilename).c_str(), m_bCreateWithFullScreen); // needs to be done after for background @@ -195,7 +201,7 @@ int SofaGLFWGUI::RegisterGUIParameters(sofa::gui::common::ArgumentParser* argume argumentParser->addArgument( cxxopts::value()->default_value("false"), "offscreen", - "(only glfw/imgui) render offscreen: no window is shown but the graphics functions are still called" + "(only glfw) render offscreen: no window is shown but the graphics functions are still called" ); return 0; } diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h index d0ec5bf3f3..ec51345cbb 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.h @@ -69,6 +69,9 @@ class SOFAGLFW_API SofaGLFWGUI : public sofa::gui::common::BaseGUI static bool isOffscreenRequested(); void setMouseButtonConfiguration(sofa::component::setting::MouseButtonSetting *setting) override; + /// An interactive GUI ignores the arguments meant for headless rendering + virtual bool isInteractive() const { return false; } + virtual BaseViewer* getViewer() override; protected: SofaGLFWBaseGUI m_baseGUI; diff --git a/SofaImGui/src/SofaImGui/ImGuiGUI.h b/SofaImGui/src/SofaImGui/ImGuiGUI.h index 3257479b12..91e5178fe4 100644 --- a/SofaImGui/src/SofaImGui/ImGuiGUI.h +++ b/SofaImGui/src/SofaImGui/ImGuiGUI.h @@ -33,6 +33,9 @@ class SOFAIMGUI_API ImGuiGUI : public sofaglfw::SofaGLFWGUI ImGuiGUI(); ~ImGuiGUI() override = default; + /// the imgui GUI is the glfw one plus interactive widgets, it is meant to be used + bool isInteractive() const override { return true; } + static sofa::gui::common::BaseGUI* CreateGUI(const char* name, sofa::simulation::NodeSPtr groot, const char* filename); }; From eece79de8713a6620eea31fd0d52ee452fa5c4b3 Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Tue, 22 Sep 2026 11:03:36 +0900 Subject: [PATCH 6/7] Start the animation when a number of iterations is given --- README.md | 3 ++- SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 869fde1270..a6c82d452b 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ Using the SHIFT + CTRL keys, you will trigger events on key pressed/released. The `-n`/`--nbIter` option of the batch GUI is also honoured by the `glfw` and `imgui` GUIs: `runSofa -g imgui -l SofaImGui -n 100 myScene.scn` stops after 100 iterations, and `infinite` -(the batch default) keeps running. +(the batch default) keeps running. As in the batch GUI, giving a number of iterations starts the +animation, `-a` is not needed. The `--offscreen` option is also available from `runSofa` with the `glfw` GUI: `runSofa -g glfw -l SofaGLFW --offscreen myScene.scn` diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp index 3981fbc53d..a9f192e94a 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp @@ -42,7 +42,13 @@ bool SofaGLFWGUI::init() int SofaGLFWGUI::mainLoop() { - m_baseGUI.runLoop(getTargetNbIterations()); + const std::size_t targetNbIterations = getTargetNbIterations(); + if (targetNbIterations > 0) + { + // a bounded run is meant to be computed, as in runSofaGLFW and the batch GUI + m_baseGUI.setSimulationIsRunning(true); + } + m_baseGUI.runLoop(targetNbIterations); return 0; } From 4427c3331d399e65dd1b277a4efc0cb7fc7408ad Mon Sep 17 00:00:00 2001 From: Frederick Roy Date: Tue, 22 Sep 2026 11:17:40 +0900 Subject: [PATCH 7/7] use the same progress bar as batch mode (same flags) --- README.md | 4 +++ SofaGLFW/CMakeLists.txt | 5 ++++ SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp | 32 +++++++++++++++++++++++ SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h | 4 +++ SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp | 8 ++++++ SofaGLFW/src/SofaGLFW/config.h.in | 1 + exe/Main.cpp | 15 +++-------- 7 files changed, 58 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a6c82d452b..4ae867d89c 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Using the SHIFT + CTRL keys, you will trigger events on key pressed/released. * `-l` or `--load`: load given plugins as a comma-separated list. Example: -l SofaPython3 * `-m` or `--msaa_samples`: set the number of samples for multisample anti-aliasing (MSAA). 0 by default. * `-n` or `--nb_iterations`: set the number of iterations to run (batch mode). 0 (unlimited) by default. +* `--hideProgressBar`: hide the progress bar shown during a bounded run. False by default. * `--offscreen`: render offscreen. No window is shown, but the graphics functions are still called. False by default. The window and its OpenGL context are created as usual, they are simply never mapped on screen. Combine it with `--nb_iterations` to get a finite run, since there is no window to close. @@ -82,6 +83,9 @@ The `-n`/`--nbIter` option of the batch GUI is also honoured by the `glfw` and ` (the batch default) keeps running. As in the batch GUI, giving a number of iterations starts the animation, `-a` is not needed. +A bounded run shows the same progress bar as the batch GUI, hidden with `--hideProgressBar`, and reports +the number of iterations, the elapsed time and the average FPS when it ends. + The `--offscreen` option is also available from `runSofa` with the `glfw` GUI: `runSofa -g glfw -l SofaGLFW --offscreen myScene.scn` (as for any GUI argument coming from a plugin, it is only listed by `runSofa --help` when the plugin is autoloaded, diff --git a/SofaGLFW/CMakeLists.txt b/SofaGLFW/CMakeLists.txt index 6a92011207..f40bc2ec6a 100644 --- a/SofaGLFW/CMakeLists.txt +++ b/SofaGLFW/CMakeLists.txt @@ -8,6 +8,7 @@ sofa_find_package(Sofa.Simulation.Common REQUIRED) sofa_find_package(Sofa.GL REQUIRED) sofa_find_package(Sofa.Component.Visual REQUIRED) sofa_find_package(Sofa.GUI.Common QUIET) +sofa_find_package(Sofa.GUI.Batch QUIET) if( UNIX AND NOT APPLE ) option(SOFAGLFW_USEX11 "Flag to force x11 for windows management" ON) @@ -78,6 +79,10 @@ if(Sofa.GUI.Common_FOUND) target_link_libraries(${PROJECT_NAME} PUBLIC Sofa.GUI.Common) endif() +if(Sofa.GUI.Batch_FOUND) + target_link_libraries(${PROJECT_NAME} PRIVATE Sofa.GUI.Batch) +endif() + sofa_find_package(SofaPython3 QUIET) if(SofaPython3_FOUND) add_subdirectory(bindings) diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp index c77842ef19..315a758d58 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.cpp @@ -50,7 +50,12 @@ #include #include +#if SOFAGLFW_HAVE_SOFA_GUI_BATCH +#include +#endif + #include +#include #include #include @@ -497,6 +502,16 @@ std::size_t SofaGLFWBaseGUI::runLoop(std::size_t targetNbIterations) std::stringstream tmpStr; std::vector pixels; +#if SOFAGLFW_HAVE_SOFA_GUI_BATCH + std::unique_ptr progressBar; + if (targetNbIterations > 0 && !m_bHideProgressBar) + { + progressBar = std::make_unique(static_cast(targetNbIterations)); + } +#endif + + const auto startTime = std::chrono::steady_clock::now(); + while (s_numberOfActiveWindows > 0 && running) { SIMULATION_LOOP_SCOPE @@ -568,10 +583,27 @@ std::size_t SofaGLFWBaseGUI::runLoop(std::size_t targetNbIterations) delete sofaGlfwWindow; } +#if SOFAGLFW_HAVE_SOFA_GUI_BATCH + if (progressBar) + { + progressBar->tick(); + } +#endif + currentNbIterations++; running = (targetNbIterations > 0) ? currentNbIterations < targetNbIterations : true; } + // measurements only make sense for a bounded run + if (targetNbIterations > 0) + { + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime).count() / 1000.0; + msg_info("SofaGLFWBaseGUI") << currentNbIterations << " iterations done in " << elapsed << " s ( " + << (elapsed > 0.0 ? static_cast(currentNbIterations) / elapsed : 0.0) + << " FPS)." << msgendl; + } + return currentNbIterations; } diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h index 6ab75d504a..62d1471310 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h @@ -63,6 +63,9 @@ class SOFAGLFW_API SofaGLFWBaseGUI : public BaseViewer void setOffscreen(bool offscreen) { m_bOffscreen = offscreen; } bool isOffscreen() const { return m_bOffscreen; } + /// Hide the progress bar of a bounded run + void setHideProgressBar(bool hide) { m_bHideProgressBar = hide; } + void initVisual(); std::size_t runLoop(std::size_t targetNbIterations = 0); void terminate(); @@ -163,6 +166,7 @@ class SOFAGLFW_API SofaGLFWBaseGUI : public BaseViewer bool m_bGlfwIsInitialized{ false }; bool m_bGlewIsInitialized{ false }; bool m_bOffscreen{ false }; + bool m_bHideProgressBar{ false }; std::unique_ptr m_glDrawTool; sofa::core::visual::VisualParams* m_vparams{ nullptr }; diff --git a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp index a9f192e94a..68d15afe84 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp @@ -48,6 +48,14 @@ int SofaGLFWGUI::mainLoop() // a bounded run is meant to be computed, as in runSofaGLFW and the batch GUI m_baseGUI.setSimulationIsRunning(true); } + + // '--hideProgressBar' is registered by the batch GUI; it is only read here + bool hideProgressBar = false; + if (s_argumentParser) + { + s_argumentParser->getValueFromKey("hideProgressBar", hideProgressBar); + } + m_baseGUI.setHideProgressBar(hideProgressBar); m_baseGUI.runLoop(targetNbIterations); return 0; } diff --git a/SofaGLFW/src/SofaGLFW/config.h.in b/SofaGLFW/src/SofaGLFW/config.h.in index 84d18b025a..160028fcd5 100644 --- a/SofaGLFW/src/SofaGLFW/config.h.in +++ b/SofaGLFW/src/SofaGLFW/config.h.in @@ -24,6 +24,7 @@ #define SOFAGLFW_VERSION @PROJECT_VERSION@ #cmakedefine01 SOFAGLFW_HAVE_SOFA_GUI_COMMON +#cmakedefine01 SOFAGLFW_HAVE_SOFA_GUI_BATCH #cmakedefine01 SOFAGLFW_HAVE_FFMPEG #cmakedefine SOFAGLFW_USEX11_INTERNAL diff --git a/exe/Main.cpp b/exe/Main.cpp index 27968f951b..dd51a53104 100644 --- a/exe/Main.cpp +++ b/exe/Main.cpp @@ -41,7 +41,6 @@ #include #include -#include int main(int argc, char** argv) { @@ -56,6 +55,7 @@ int main(int argc, char** argv) ("m,msaa_samples", "set number of samples for multisample anti-aliasing (MSAA)", cxxopts::value()->default_value("0")) ("n,nb_iterations", "set number of iterations to run (batch mode)", cxxopts::value()->default_value("0")) ("offscreen", "render offscreen: no window is shown but the graphics functions are still called", cxxopts::value()->default_value("false")) + ("hideProgressBar", "hide the progress bar of a bounded run", cxxopts::value()->default_value("false")) ("h,help", "print usage") ; @@ -176,17 +176,10 @@ int main(int argc, char** argv) glfwGUI.setWindowBackgroundImage(background->d_image.getFullPath()); } - // Run the main loop - const auto currentTime = std::chrono::steady_clock::now(); - const auto currentNbIterations = glfwGUI.runLoop(targetNbIterations); + // Run the main loop; it reports the measurements of a bounded run itself + glfwGUI.setHideProgressBar(result["hideProgressBar"].as()); + glfwGUI.runLoop(targetNbIterations); - const auto totalTime = std::chrono::duration_cast(std::chrono::steady_clock::now() - currentTime).count() / 1000.0; - - // measurements only make sense in batch mode - if (targetNbIterations > 0) - { - msg_info("SofaGLFW") << currentNbIterations << " iterations done in " << totalTime << " s ( " << (static_cast(currentNbIterations) / totalTime) << " FPS)." << msgendl; - } if (groot != nullptr) {