diff --git a/README.md b/README.md index 962d357017..4ae867d89c 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,28 @@ 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. +* `--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. + +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. 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, +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 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 950ddfcaee..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 @@ -251,6 +256,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 +289,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 +398,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 @@ -481,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 @@ -552,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 da5d53876b..62d1471310 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWBaseGUI.h @@ -57,6 +57,15 @@ 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; } + + /// 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(); @@ -156,6 +165,8 @@ 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 fb959267b1..68d15afe84 100644 --- a/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp +++ b/SofaGLFW/src/SofaGLFW/SofaGLFWGUI.cpp @@ -25,12 +25,16 @@ #include #include #include +#include +#include using namespace sofa; namespace sofaglfw { +sofa::gui::common::ArgumentParser* SofaGLFWGUI::s_argumentParser = nullptr; + bool SofaGLFWGUI::init() { return m_baseGUI.init(); @@ -38,10 +42,59 @@ bool SofaGLFWGUI::init() int SofaGLFWGUI::mainLoop() { - m_baseGUI.runLoop(); + 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); + } + + // '--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; } +bool SofaGLFWGUI::isOffscreenRequested() +{ + bool offscreen = false; + if (s_argumentParser) + { + s_argumentParser->getValueFromKey("offscreen", offscreen); + } + 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() { @@ -63,6 +116,13 @@ void SofaGLFWGUI::setScene(sofa::simulation::NodeSPtr groot, const char* filenam m_baseGUI.setSimulation(groot, strFilename); m_baseGUI.load(); + 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 @@ -140,6 +200,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) 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..ec51345cbb 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,28 @@ 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); + + /// 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; + /// An interactive GUI ignores the arguments meant for headless rendering + virtual bool isInteractive() const { return false; } + 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/SofaGLFWWindow.cpp b/SofaGLFW/src/SofaGLFW/SofaGLFWWindow.cpp index 9e7452481a..033a1f77be 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(); @@ -169,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/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/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/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); }; 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/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 40e080fca7..dd51a53104 100644 --- a/exe/Main.cpp +++ b/exe/Main.cpp @@ -28,15 +28,19 @@ #include #include #include +#include #include #include +#include #include #include #include +#include +#include #include +#include -#include int main(int argc, char** argv) { @@ -50,6 +54,8 @@ 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")) + ("hideProgressBar", "hide the progress bar of a bounded run", cxxopts::value()->default_value("false")) ("h,help", "print usage") ; @@ -66,6 +72,12 @@ int main(int argc, char** argv) sofa::helper::BackTrace::autodump(); + 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; @@ -78,9 +90,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 +118,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; @@ -112,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); @@ -123,12 +151,20 @@ 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); 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); @@ -140,22 +176,17 @@ 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); - - const auto totalTime = std::chrono::duration_cast(std::chrono::steady_clock::now() - currentTime).count() / 1000.0; + // Run the main loop; it reports the measurements of a bounded run itself + glfwGUI.setHideProgressBar(result["hideProgressBar"].as()); + glfwGUI.runLoop(targetNbIterations); - // 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) { sofa::simulation::node::unload(groot); } + sofa::simulation::common::cleanup(); + return 0; }