/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2025 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WIN32 #include #include #include "Psapi.h" #endif // WIN32 #ifdef __APPLE__ #include #endif // __APPLE__ #include "openspaceengine_lua.inl" namespace { // Helper structs for the visitor pattern of the std::variant template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; constexpr std::string_view _loggerCat = "OpenSpaceEngine"; #ifdef WIN32 // This counter is used to measure the VRAM usage of OpenSpace PDH_HQUERY vramQuery; PDH_HCOUNTER vramCounter; #endif // WIN32 constexpr std::string_view stringify(openspace::OpenSpaceEngine::Mode m) { using Mode = openspace::OpenSpaceEngine::Mode; switch (m) { case Mode::UserControl: return "UserControl"; case Mode::CameraPath: return "CameraPath"; case Mode::SessionRecordingPlayback: return "SessionRecording"; } throw ghoul::MissingCaseException(); } constexpr openspace::properties::Property::PropertyInfo PrintEventsInfo = { "PrintEvents", "Print events", "If this is enabled, all events that are propagated through the system are " "printed to the log.", openspace::properties::Property::Visibility::AdvancedUser }; constexpr openspace::properties::Property::PropertyInfo VisibilityInfo = { "PropertyVisibility", "Property visibility", "Hides or displays different settings in the GUI depending on how advanced they " "are.", openspace::properties::Property::Visibility::Always }; constexpr openspace::properties::Property::PropertyInfo FadeDurationInfo = { "FadeDuration", "Fade duration (seconds)", "Controls how long time the fading in/out takes when enabling/disabling an " "object through a checkbox in the UI. Holding SHIFT while clicking the " "checkbox will enable/disable the renderable without fading, as will setting " "this value to zero.", openspace::properties::Property::Visibility::AdvancedUser }; constexpr openspace::properties::Property::PropertyInfo DisableMouseInputInfo = { "DisableMouseInputs", "Disable all mouse inputs", "Disables all mouse inputs. Useful when using touch interaction, to prevent " "double inputs on touch (from both touch input and inserted mouse inputs).", openspace::properties::Property::Visibility::User, openspace::properties::Property::NeedsConfirmation::Yes }; constexpr openspace::properties::Property::PropertyInfo ShowPropertyConfirmationDialogInfo = { "ShowPropertyConfirmation", "Show property confirmation", "Controls whether confirmation dialogs are shown when making changes to certain " "properties. If checked, the dialogs will be shown for properties that require " "it. If unchecked, no dialogs will be shown.", openspace::properties::Property::Visibility::AdvancedUser }; void viewportChanged() { using namespace openspace; // Needs to be updated since each render call potentially targets a different // window and/or viewport using FR = ghoul::fontrendering::FontRenderer; FR::defaultRenderer().setFramebufferSize(global::renderEngine->fontResolution()); FR::defaultProjectionRenderer().setFramebufferSize( global::renderEngine->renderingResolution() ); } void resetPropertyChangeFlagsOfSubowners(openspace::properties::PropertyOwner* po) { using namespace openspace; for (properties::PropertyOwner* subOwner : po->propertySubOwners()) { resetPropertyChangeFlagsOfSubowners(subOwner); } for (properties::Property* p : po->properties()) { p->resetToUnchanged(); } } void resetPropertyChangeFlags() { using namespace openspace; ZoneScoped; Scene* scene = global::renderEngine->scene(); if (!scene) { return; } const std::vector nodes = scene->allSceneGraphNodes(); for (SceneGraphNode* n : nodes) { resetPropertyChangeFlagsOfSubowners(n); } } } // namespace namespace openspace { class Scene; OpenSpaceEngine::OpenSpaceEngine() : properties::PropertyOwner({ "OpenSpaceEngine", "OpenSpace Engine" }) , _printEvents(PrintEventsInfo, false) , _visibility(VisibilityInfo) , _showPropertyConfirmationDialog(ShowPropertyConfirmationDialogInfo, true) , _fadeOnEnableDuration(FadeDurationInfo, 1.f, 0.f, 5.f) , _disableAllMouseInputs(DisableMouseInputInfo, false) { FactoryManager::initialize(); SpiceManager::initialize(); TransformationManager::initialize(); addProperty(_printEvents); using Visibility = openspace::properties::Property::Visibility; _visibility.addOptions({ { static_cast(Visibility::NoviceUser), "Novice User" }, { static_cast(Visibility::User), "User" }, { static_cast(Visibility::AdvancedUser), "Advanced User" }, { static_cast(Visibility::Developer), "Developer" }, { static_cast(Visibility::Hidden), "Everything" }, }); addProperty(_visibility); addProperty(_showPropertyConfirmationDialog); addProperty(_fadeOnEnableDuration); addProperty(_disableAllMouseInputs); ghoul::TemplateFactory* fTask = FactoryManager::ref().factory(); ghoul_assert(fTask, "No task factory existed"); fTask->registerClass("ConvertRecFormatTask"); #ifdef WIN32 PDH_STATUS status = PdhOpenQueryA(nullptr, 0, &vramQuery); if (status != ERROR_SUCCESS) { LWARNING("Error opening Performance Query for VRAM usage"); } const std::string queryStr = std::format( "\\GPU Process Memory(pid_{}*)\\Dedicated Usage", GetCurrentProcessId() ); status = PdhAddEnglishCounterA(vramQuery, queryStr.c_str(), 0, &vramCounter); if (status != ERROR_SUCCESS) { LWARNING("Error add Performance Query for VRAM usage"); } #endif // WIN32 } OpenSpaceEngine::~OpenSpaceEngine() {} void OpenSpaceEngine::registerPathTokens() { LTRACE("OpenSpaceEngine::initialize(begin)"); // Registering Path tokens. If the BASE path is set, it is the only one that will // overwrite the default path of the cfg directory using T = std::string; for (const std::pair& path : global::configuration->pathTokens) { std::string fullKey = "${" + path.first + "}"; LDEBUG(std::format("Registering path '{}': {}", fullKey, path.second)); const bool overrideBase = (fullKey == "${BASE}"); if (overrideBase) { LINFO(std::format("Overriding base path with '{}'", path.second)); } const bool overrideTemporary = (fullKey == "${TEMPORARY}"); using Override = ghoul::filesystem::FileSystem::Override; FileSys.registerPathToken( std::move(fullKey), path.second, Override(overrideBase || overrideTemporary) ); } LTRACE("OpenSpaceEngine::initialize(end)"); } void OpenSpaceEngine::initialize() { ZoneScoped; LTRACE("OpenSpaceEngine::initialize(begin)"); global::initialize(); // Initialize the general capabilities component SysCap.addComponent( std::make_unique() ); _printEvents = global::configuration->isPrintingEvents; _visibility = static_cast(global::configuration->propertyVisibility); _showPropertyConfirmationDialog = global::configuration->showPropertyConfirmation; std::filesystem::path cacheFolder = absPath("${CACHE}"); if (global::configuration->usePerProfileCache) { cacheFolder = std::format("{}-{}", cacheFolder, global::configuration->profile); LINFO(std::format("Old cache: {}", absPath("${CACHE}"))); LINFO(std::format("New cache: {}", cacheFolder)); FileSys.registerPathToken( "${CACHE}", cacheFolder, ghoul::filesystem::FileSystem::Override::Yes ); } // Create directories that doesn't exist for (const std::string& token : FileSys.tokens()) { if (!std::filesystem::is_directory(absPath(token))) { std::filesystem::create_directories(absPath(token)); } } try { FileSys.createCacheManager(cacheFolder); } catch (const ghoul::RuntimeError& e) { LFATAL("Could not create Cache Manager"); LFATALC(e.component, e.message); } // Initialize the requested logs from the configuration file // We previously initialized the LogManager with a console log to provide some logging // until we know which logs should be added if (ghoul::logging::LogManager::isInitialized()) { ghoul::logging::LogManager::deinitialize(); } const ghoul::logging::LogLevel level = ghoul::from_string( global::configuration->logging.level ); const bool immediateFlush = global::configuration->logging.forceImmediateFlush; using ImmediateFlush = ghoul::logging::LogManager::ImmediateFlush; ghoul::logging::LogManager::initialize(level, ImmediateFlush(immediateFlush)); for (const ghoul::Dictionary& log : global::configuration->logging.logs) { try { LogMgr.addLog(createLog(log)); } catch (const documentation::SpecificationError& e) { LERROR("Failed loading of log"); logError(e); throw; } } #ifdef WIN32 if (IsDebuggerPresent()) { LogMgr.addLog(std::make_unique()); } #endif // WIN32 #ifndef GHOUL_LOGGING_ENABLE_TRACE if (level == ghoul::logging::LogLevel::Trace) { LWARNING( "Desired logging level is set to 'Trace' but application was " "compiled without Trace support" ); } #endif // GHOUL_LOGGING_ENABLE_TRACE if (!global::configuration->scriptLog.empty() && global::configuration->scriptLogRotation > 0) { int rot = global::configuration->scriptLogRotation; while (rot > 0) { // Move all of the existing logs one position up const std::filesystem::path file = absPath(global::configuration->scriptLog); const std::filesystem::path fname = file.stem(); const std::filesystem::path ext = file.extension(); std::filesystem::path newCandidate = file; newCandidate.replace_filename(std::format("{}-{}{}", fname, rot, ext)); std::filesystem::path oldCandidate = file; if (rot > 1) { // We don't actually have a -0 version, it is just the base name oldCandidate.replace_filename( std::format("{}-{}{}", fname, rot - 1, ext) ); } if (std::filesystem::exists(newCandidate)) { std::filesystem::remove(newCandidate); } if (std::filesystem::exists(oldCandidate)) { std::filesystem::rename(oldCandidate, newCandidate); } rot--; } } LINFOC("OpenSpace Version", std::string(OPENSPACE_VERSION)); LINFOC("Commit", std::string(OPENSPACE_GIT_FULL)); // Register modules global::moduleEngine->initialize(global::configuration->moduleConfigurations); // After registering the modules, the documentations for the available classes // can be added as well for (documentation::Documentation d : global::moduleEngine->moduleDocumentations()) { DocEng.addDocumentation(std::move(d)); } for (OpenSpaceModule* m : global::moduleEngine->modules()) { for (const documentation::Documentation& doc : m->documentations()) { DocEng.addDocumentation(doc); } } DocEng.addDocumentation(Configuration::Documentation()); // Register the provided shader directories ghoul::opengl::ShaderPreprocessor::addIncludePath(absPath("${SHADERS}")); if (!global::configuration->sandboxedLua) { // The Lua state is sandboxed by default, so if the user wants an unsandboxed one, // we have to recreate it here. // @TODO (2024-08-07, abock) It's not pretty, but doing it differently would // require a bigger rewrite of how we handle the ScriptEngine global::scriptEngine->~ScriptEngine(); global::scriptEngine = new (global::scriptEngine) scripting::ScriptEngine( global::configuration->sandboxedLua ); } // Register Lua script functions LDEBUG("Registering Lua libraries"); registerCoreClasses(*global::scriptEngine); // Process profile file std::filesystem::path profile; if (!std::filesystem::is_regular_file(global::configuration->profile)) { const std::filesystem::path userCandidate = absPath(std::format( "${{USER_PROFILES}}/{}.profile", global::configuration->profile )); const std::filesystem::path profileCandidate = absPath(std::format( "${{PROFILES}}/{}.profile", global::configuration->profile )); // Give the user profile priority if there are both if (std::filesystem::is_regular_file(userCandidate)) { profile = userCandidate; } else if (std::filesystem::is_regular_file(profileCandidate)) { profile = profileCandidate; } else { throw ghoul::RuntimeError(std::format( "Could not load profile '{}': File does not exist", global::configuration->profile )); } } else { profile = global::configuration->profile; } // Load the profile LINFO(std::format("Loading profile '{}'", profile)); *global::profile = Profile(profile); // Set up asset loader _assetManager = std::make_unique( global::scriptEngine->luaState(), absPath("${ASSETS}") ); global::scriptEngine->addLibrary(_assetManager->luaLibrary()); for (OpenSpaceModule* module : global::moduleEngine->modules()) { global::scriptEngine->addLibrary(module->luaLibrary()); for (const scripting::LuaLibrary& l : module->luaLibraries()) { global::scriptEngine->addLibrary(l); } } global::scriptEngine->initialize(); _shutdown.waitTime = global::configuration->shutdownCountdown; global::navigationHandler->initialize(); global::renderEngine->initialize(); for (const std::function& func : *global::callback::initialize) { ZoneScopedN("[Module] initialize"); func(); } LTRACE("OpenSpaceEngine::initialize(end)"); } void OpenSpaceEngine::initializeGL() { ZoneScoped; LTRACE("OpenSpaceEngine::initializeGL(begin)"); glbinding::Binding::initialize(global::windowDelegate->openGLProcedureAddress); //glbinding::Binding::useCurrentContext(); LDEBUG("Adding OpenGL capabilities components"); // Detect and log OpenCL and OpenGL versions and available devices SysCap.addComponent( std::make_unique() ); LDEBUG("Detecting capabilities"); SysCap.detectCapabilities(); using Verbosity = ghoul::systemcapabilities::SystemCapabilitiesComponent::Verbosity; const Verbosity verbosity = ghoul::from_string( global::configuration->logging.capabilitiesVerbosity ); SysCap.logCapabilities(verbosity); const std::string versionCheckUrl = global::configuration->versionCheckUrl; if (!versionCheckUrl.empty()) { global::versionChecker->requestLatestVersion(versionCheckUrl); } // Check the required OpenGL versions of the registered modules const ghoul::systemcapabilities::Version version = global::moduleEngine->requiredOpenGLVersion(); LINFO(std::format("Required OpenGL version: {}", ghoul::to_string(version))); if (OpenGLCap.openGLVersion() < version) { throw ghoul::RuntimeError( "An included module required a higher OpenGL version than is supported on " "this system", "OpenSpaceEngine" ); } { // Check the available OpenGL extensions against the required extensions using OCC = ghoul::systemcapabilities::OpenGLCapabilitiesComponent; for (OpenSpaceModule* m : global::moduleEngine->modules()) { for (const std::string& ext : m->requiredOpenGLExtensions()) { if (!SysCap.component().isExtensionSupported(ext)) { LFATAL(std::format( "Module '{}' required OpenGL extension '{}' which is not " "available on this system. Some functionality related to this " "module will probably not work", m->guiName(), ext )); } } } } rendering::helper::initialize(); loadFonts(); _loadingScreen = std::make_unique( LoadingScreen::ShowMessage( global::configuration->loadingScreen.isShowingMessages ), LoadingScreen::ShowNodeNames( global::configuration->loadingScreen.isShowingNodeNames ), LoadingScreen::ShowLogMessages( global::configuration->loadingScreen.isShowingLogMessages ) ); _loadingScreen->render(); LTRACE("OpenSpaceEngine::initializeGL::Console::initialize(begin)"); try { global::luaConsole->initialize(); global::luaConsole->setCommandInputButton(global::configuration->consoleKey); } catch (const ghoul::RuntimeError& e) { LERROR("Error initializing Console with error:"); LERRORC(e.component, e.message); } LTRACE("OpenSpaceEngine::initializeGL::Console::initialize(end)"); LTRACE("OpenSpaceEngine::initializeGL::DebugContext(begin)"); bool debugActive = global::configuration->openGLDebugContext.isActive; // Debug output is not available before 4.3 const ghoul::systemcapabilities::Version minVersion = { .major = 4, .minor = 3, .release = 0 }; if (debugActive && OpenGLCap.openGLVersion() < minVersion) { LINFO("OpenGL Debug context requested, but insufficient version available"); debugActive = false; } if (debugActive) { using namespace ghoul::opengl::debug; const bool synchronous = global::configuration->openGLDebugContext.isSynchronous; setDebugOutput(DebugOutput(debugActive), SynchronousOutput(synchronous)); for (const Configuration::OpenGLDebugContext::IdentifierFilter& f : global::configuration->openGLDebugContext.identifierFilters) { setDebugMessageControl( ghoul::from_string(f.source), ghoul::from_string(f.type), { f.identifier }, Enabled::No ); } for (const std::string& sev : global::configuration->openGLDebugContext.severityFilters) { setDebugMessageControl( Source::DontCare, Type::DontCare, ghoul::from_string(sev), Enabled::No ); } ghoul::opengl::debug::setDebugCallback( [](Source source, Type type, Severity severity, unsigned int id, const std::string& message) { const std::string s = ghoul::to_string(source); const std::string t = ghoul::to_string(type); const std::string cat = std::format("OpenGL ({}) [{}] {{{}}}", s, t, id); switch (severity) { case Severity::High: LERRORC(cat, message); break; case Severity::Medium: LWARNINGC(cat, message); break; case Severity::Low: LINFOC(cat, message); break; case Severity::Notification: LDEBUGC(cat, message); break; default: throw ghoul::MissingCaseException(); } if (global::configuration->openGLDebugContext.printStacktrace) { std::string stackString = "Stacktrace\n"; std::vector stack = ghoul::stackTrace(); for (size_t i = 0; i < stack.size(); i++) { stackString += std::format("{}: {}\n", i, stack[i]); } LDEBUGC(cat, stackString); } } ); } LTRACE("OpenSpaceEngine::initializeGL::DebugContext(end)"); // The ordering of the KeyCheckOpenGLState and KeyLogEachOpenGLCall are important as // the callback mask in glbinding is stateful for each context, and since // KeyLogEachOpenGLCall is more specific, we want it to be able to overwrite the // state from KeyCheckOpenGLState if (global::configuration->isCheckingOpenGLState) { using namespace glbinding; // Infinite loop -- welcome to the danger zone setCallbackMaskExcept(CallbackMask::After, { "glGetError" }); setAfterCallback([](const FunctionCall& f) { const GLenum error = glGetError(); switch (error) { case GL_NO_ERROR: break; case GL_INVALID_ENUM: LERRORC( "OpenGL Invalid State", std::format("Function '{}': GL_INVALID_ENUM", f.function->name()) ); break; case GL_INVALID_VALUE: LERRORC( "OpenGL Invalid State", std::format("Function '{}': GL_INVALID_VALUE", f.function->name()) ); break; case GL_INVALID_OPERATION: LERRORC( "OpenGL Invalid State", std::format( "Function '{}': GL_INVALID_OPERATION", f.function->name() )); break; case GL_INVALID_FRAMEBUFFER_OPERATION: LERRORC( "OpenGL Invalid State", std::format( "Function '{}': GL_INVALID_FRAMEBUFFER_OPERATION", f.function->name() ) ); break; case GL_OUT_OF_MEMORY: LERRORC( "OpenGL Invalid State", std::format("Function '{}': GL_OUT_OF_MEMORY", f.function->name()) ); break; default: LERRORC( "OpenGL Invalid State", std::format("Unknown error code: {0:x}", static_cast(error)) ); } }); } if (global::configuration->isLoggingOpenGLCalls) { using namespace ghoul::logging; const LogLevel lvl = ghoul::from_string( global::configuration->logging.level ); if (lvl > LogLevel::Trace) { LWARNING( "Logging OpenGL calls is enabled, but the selected log level does " "not include TRACE, so no OpenGL logs will be printed" ); } else { using namespace glbinding; setCallbackMask(CallbackMask::After | CallbackMask::ParametersAndReturnValue); glbinding::setAfterCallback([](const glbinding::FunctionCall& call) { std::string arguments = std::accumulate( call.parameters.begin(), call.parameters.end(), std::string("("), [](const std::string& a, const std::unique_ptr& v) { std::stringstream s; s << v.get(); return a + s.str() + ", "; } ); // Remove the final ", " arguments = arguments.substr(0, arguments.size() - 2) + ")"; std::string returnValue; std::stringstream s; if (call.returnValue) { s << call.returnValue.get(); returnValue = " -> " + s.str(); } LTRACEC( "OpenGL", call.function->name() + std::move(arguments) + std::move(returnValue) ); }); } } LDEBUG("Initializing Rendering Engine"); global::renderEngine->initializeGL(); global::moduleEngine->initializeGL(); for (const std::function& func : *global::callback::initializeGL) { ZoneScopedN("[Module] initializeGL"); func(); } LINFO("Finished initializing OpenGL"); LTRACE("OpenSpaceEngine::initializeGL(end)"); } void OpenSpaceEngine::loadAssets() { ZoneScoped; LTRACE("OpenSpaceEngine::loadAsset(begin)"); global::windowDelegate->setBarrier(false); global::windowDelegate->setSynchronization(false); defer { global::windowDelegate->setSynchronization(true); global::windowDelegate->setBarrier(true); }; std::unique_ptr sceneInitializer; if (global::configuration->useMultithreadedInitialization) { const unsigned int nThreads = std::max( std::thread::hardware_concurrency() / 2, 2u ); sceneInitializer = std::make_unique(nThreads); } else { sceneInitializer = std::make_unique(); } _scene = std::make_unique(std::move(sceneInitializer)); global::renderEngine->setScene(_scene.get()); global::rootPropertyOwner->addPropertySubOwner(_scene.get()); Camera* camera = _scene->camera(); global::renderEngine->setCamera(camera); global::navigationHandler->setCamera(camera); const SceneGraphNode* parent = camera->parent(); if (parent) { global::navigationHandler->orbitalNavigator().setFocusNode(parent->identifier()); } else { global::navigationHandler->orbitalNavigator().setFocusNode( _scene->root()->identifier() ); } for (const std::string& a : global::profile->assets) { _assetManager->add(a); } _loadingScreen->exec(*_assetManager, *_scene); _loadingScreen = nullptr; global::renderEngine->updateScene(); global::syncEngine->addSyncables(global::timeManager->syncables()); global::syncEngine->addSyncables( global::navigationHandler->orbitalNavigator().syncables() ); if (_scene && _scene->camera()) { global::syncEngine->addSyncables(_scene->camera()->syncables()); } #ifdef __APPLE__ showTouchbar(); #endif // APPLE runGlobalCustomizationScripts(); _writeDocumentationTask = std::async( &documentation::DocumentationEngine::writeJavascriptDocumentation, DocEng ); LTRACE("OpenSpaceEngine::loadAsset(end)"); } void OpenSpaceEngine::deinitialize() { ZoneScoped; LTRACE("OpenSpaceEngine::deinitialize(begin)"); { // We are storing the `hasStartedBefore` setting here instead of in the // intialization phase as otherwise we'd always think that OpenSpace had been // started before openspace::Settings settings = loadSettings(); settings.hasStartedBefore = true; const date::year_month_day now = date::year_month_day( floor(std::chrono::system_clock::now()) ); settings.lastStartedDate = std::format( "{}-{:0>2}-{:0>2}", static_cast(now.year()), static_cast(now.month()), static_cast(now.day()) ); saveSettings(settings, findSettings()); } for (const std::function& func : *global::callback::deinitialize) { func(); } global::navigationHandler->deinitialize(); LTRACE("deinitialize(begin)"); if (global::parallelPeer->status() != ParallelConnection::Status::Disconnected) { global::parallelPeer->disconnect(); } if (global::renderEngine->scene() && global::renderEngine->scene()->camera()) { global::syncEngine->removeSyncables( global::renderEngine->scene()->camera()->syncables() ); } global::versionChecker->cancel(); _assetManager = nullptr; global::deinitialize(); FactoryManager::deinitialize(); TransformationManager::deinitialize(); SpiceManager::deinitialize(); if (_printEvents) { events::Event* e = global::eventEngine->firstEvent(); events::logAllEvents(e); } ghoul::fontrendering::FontRenderer::deinitialize(); ghoul::logging::LogManager::deinitialize(); LTRACE("deinitialize(end)"); LTRACE("OpenSpaceEngine::deinitialize(end)"); } void OpenSpaceEngine::deinitializeGL() { ZoneScoped; LTRACE("OpenSpaceEngine::deinitializeGL(begin)"); viewportChanged(); // We want to render an image informing the user that we are shutting down global::renderEngine->renderEndscreen(); global::windowDelegate->swapBuffer(); global::openSpaceEngine->assetManager().deinitialize(); global::openSpaceEngine->_scene = nullptr; global::renderEngine->setScene(nullptr); for (const std::function& func : *global::callback::deinitializeGL) { func(); } _loadingScreen = nullptr; global::deinitializeGL(); rendering::helper::deinitialize(); LTRACE("OpenSpaceEngine::deinitializeGL(end)"); } void OpenSpaceEngine::createUserDirectoriesIfNecessary() { LTRACE(absPath("${USER}").string()); if (!std::filesystem::exists(absPath("${USER_ASSETS}"))) { std::filesystem::create_directories(absPath("${USER_ASSETS}")); } if (!std::filesystem::exists(absPath("${USER_PROFILES}"))) { std::filesystem::create_directories(absPath("${USER_PROFILES}")); } if (!std::filesystem::exists(absPath("${USER_CONFIG}"))) { std::filesystem::create_directories(absPath("${USER_CONFIG}")); } if (!std::filesystem::is_directory(absPath("${USER_WEBPANELS}"))) { std::filesystem::create_directories(absPath("${USER_WEBPANELS}")); } } uint64_t OpenSpaceEngine::ramInUse() const { #ifdef WIN32 PROCESS_MEMORY_COUNTERS_EX pmc; BOOL success = GetProcessMemoryInfo( GetCurrentProcess(), reinterpret_cast(&pmc), sizeof(PROCESS_MEMORY_COUNTERS_EX) ); if (!success) { LERROR("Error retrieving RAM usage"); return 0; } return pmc.PrivateUsage; #else // ^^^^ WIN32 // !WIN32 vvvv LWARNING("Unsupported operating"); return 0; #endif } uint64_t OpenSpaceEngine::vramInUse() const { #ifdef WIN32 PDH_STATUS status = PdhCollectQueryData(vramQuery); if (status != ERROR_SUCCESS) { LERROR("Error collecting VRAM query data"); return 0; } PDH_FMT_COUNTERVALUE value; status = PdhGetFormattedCounterValue( vramCounter, PDH_FMT_LARGE | PDH_FMT_NOSCALE, nullptr, &value ); if (status != ERROR_SUCCESS) { LERROR("Error formatting VRAM query data"); return 0; } LONGLONG v = value.largeValue; return v; #else // ^^^^ WIN32 // !WIN32 vvvv LWARNING("Unsupported operating"); return 0; #endif } void OpenSpaceEngine::runGlobalCustomizationScripts() { ZoneScoped; LINFO("Running Global initialization scripts"); const ghoul::lua::LuaState state = ghoul::lua::LuaState( ghoul::lua::LuaState::Sandboxed::No ); global::scriptEngine->initializeLuaState(state); for (const std::string& script : global::configuration->globalCustomizationScripts) { std::filesystem::path s = absPath(script); if (std::filesystem::is_regular_file(s)) { try { LINFO(std::format("Running global customization script: {}", s)); ghoul::lua::runScriptFile(state, s); } catch (const ghoul::RuntimeError& e) { LERRORC(e.component, e.message); } } else { LDEBUG(std::format("Ignoring non-existing script file: {}", s)); } } } void OpenSpaceEngine::loadFonts() { global::fontManager->initialize(); using T = std::string; for (const std::pair& font : global::configuration->fonts) { std::string key = font.first; std::filesystem::path fontName = absPath(font.second); if (!std::filesystem::is_regular_file(fontName)) { LERROR(std::format("Could not find font '{}' for key '{}'", fontName, key)); continue; } LDEBUG(std::format("Registering font '{}' with key '{}'", fontName, key)); global::fontManager->registerFontPath(key, fontName); } try { ghoul::fontrendering::FontRenderer::initialize(); } catch (const ghoul::RuntimeError& err) { LERRORC(err.component, err.message); } } void OpenSpaceEngine::preSynchronization() { ZoneScoped; TracyGpuZone("preSynchronization"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE LTRACE("OpenSpaceEngine::preSynchronization(begin)"); FileSys.triggerFilesystemEvents(); // Reset the temporary, frame-based storage global::memoryManager->TemporaryMemory.reset(); if (_isRenderingFirstFrame) { global::profile->ignoreUpdates = true; loadAssets(); global::renderEngine->scene()->setPropertiesFromProfile(*global::profile); global::timeManager->setTimeFromProfile(*global::profile); global::timeManager->setDeltaTimeSteps(global::profile->deltaTimes); setActionsFromProfile(*global::profile); setKeybindingsFromProfile(*global::profile); setModulesFromProfile(*global::profile); setMarkInterestingNodesFromProfile(*global::profile); global::profile->ignoreUpdates = false; resetPropertyChangeFlagsOfSubowners(global::rootPropertyOwner); global::windowDelegate->setSynchronization(false); } const bool master = global::windowDelegate->isMaster(); global::syncEngine->preSynchronization(SyncEngine::IsMaster(master)); if (master) { const double dt = global::sessionRecordingHandler->isSavingFramesDuringPlayback() ? global::sessionRecordingHandler->fixedDeltaTimeDuringFrameOutput() : global::windowDelegate->deltaTime(); global::timeManager->preSynchronization(dt); const std::vector schedScripts = global::scriptScheduler->progressTo( global::timeManager->time().j2000Seconds() ); for (const std::string& script : schedScripts) { if (script.empty()) { continue; } global::scriptEngine->queueScript(script); global::eventEngine->publishEvent( script ); } global::renderEngine->updateScene(); if (_scene) { Camera* camera = _scene->camera(); if (camera) { global::navigationHandler->updateCamera(dt); camera->invalidateCache(); } } global::sessionRecordingHandler->preSynchronization(dt); global::parallelPeer->preSynchronization(); global::interactionMonitor->updateActivityState(); } for (const std::function& func : *global::callback::preSync) { ZoneScopedN("[Module] preSync"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE func(); } if (_isRenderingFirstFrame) { setCameraFromProfile(*global::profile); setAdditionalScriptsFromProfile(*global::profile); } // Handle callback(s) for change in engine mode if (_modeLastFrame != _currentMode) { using K = CallbackHandle; using V = ModeChangeCallback; for (const std::pair& it : _modeChangeCallbacks) { it.second(); } } _modeLastFrame = _currentMode; LTRACE("OpenSpaceEngine::preSynchronization(end)"); } void OpenSpaceEngine::postSynchronizationPreDraw() { ZoneScoped; TracyGpuZone("postSynchronizationPreDraw"); LTRACE("OpenSpaceEngine::postSynchronizationPreDraw(begin)"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE const bool master = global::windowDelegate->isMaster(); global::syncEngine->postSynchronization(SyncEngine::IsMaster(master)); if (_shutdown.inShutdown) { if (_shutdown.timer <= 0.f) { global::eventEngine->publishEvent( events::EventApplicationShutdown::State::Finished ); global::windowDelegate->terminate(); return; } _shutdown.timer -= static_cast(global::windowDelegate->averageDeltaTime()); } _assetManager->update(); global::renderEngine->updateScene(); global::renderEngine->updateRenderer(); global::renderEngine->updateScreenSpaceRenderables(); global::renderEngine->updateShaderPrograms(); global::luaConsole->update(); if (!master) { global::navigationHandler->orbitalNavigator().updateAnchorOnSync(); _scene->camera()->invalidateCache(); } for (const std::function& func : *global::callback::postSyncPreDraw) { ZoneScopedN("[Module] postSyncPreDraw"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE func(); } // Testing this every frame has minimal impact on the performance --- abock // Debug build: 1-2 us ; Release build: <= 1 us using ghoul::logging::LogManager; int warningCounter = LogMgr.messageCounter(ghoul::logging::LogLevel::Warning); int errorCounter = LogMgr.messageCounter(ghoul::logging::LogLevel::Error); int fatalCounter = LogMgr.messageCounter(ghoul::logging::LogLevel::Fatal); if (warningCounter > 0) { LWARNINGC("Logging", std::format("Number of Warnings: {}", warningCounter)); } if (errorCounter > 0) { LWARNINGC("Logging", std::format("Number of Errors: {}", errorCounter)); } if (fatalCounter > 0) { LWARNINGC("Logging", std::format("Number of Fatals: {}", fatalCounter)); } LogMgr.resetMessageCounters(); LTRACE("OpenSpaceEngine::postSynchronizationPreDraw(end)"); } void OpenSpaceEngine::render(const glm::mat4& sceneMatrix, const glm::mat4& viewMatrix, const glm::mat4& projectionMatrix) { ZoneScoped; TracyGpuZone("Render"); LTRACE("OpenSpaceEngine::render(begin)"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE viewportChanged(); global::renderEngine->render(sceneMatrix, viewMatrix, projectionMatrix); for (const std::function& func : *global::callback::render) { ZoneScopedN("[Module] render"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE func(); } LTRACE("OpenSpaceEngine::render(end)"); } void OpenSpaceEngine::drawOverlays() { ZoneScoped; TracyGpuZone("Draw2D"); const ghoul::GLDebugGroup group("Overlays"); LTRACE("OpenSpaceEngine::drawOverlays(begin)"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE viewportChanged(); // Render the overlays on top of the rendered scene const bool isGuiWindow = global::windowDelegate->hasGuiWindow() ? global::windowDelegate->isGuiWindow() : true; if (isGuiWindow) { global::renderEngine->renderOverlays(_shutdown); global::sessionRecordingHandler->render(); global::navigationHandler->renderOverlay(); } for (const std::function& func : *global::callback::draw2D) { ZoneScopedN("[Module] draw2D"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE func(); } if (isGuiWindow) { global::luaConsole->render(); } LTRACE("OpenSpaceEngine::drawOverlays(end)"); } void OpenSpaceEngine::postDraw() { ZoneScoped; TracyGpuZone("postDraw"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE LTRACE("OpenSpaceEngine::postDraw(begin)"); global::renderEngine->postDraw(); for (const std::function& func : *global::callback::postDraw) { ZoneScopedN("[Module] postDraw"); #ifdef TRACY_ENABLE TracyPlot("RAM", static_cast(ramInUse())); TracyPlot("VRAM", static_cast(vramInUse())); #endif // TRACY_ENABLE func(); } if (_isRenderingFirstFrame) { global::windowDelegate->setSynchronization(true); resetPropertyChangeFlags(); _isRenderingFirstFrame = false; } // // Handle events // const events::Event* e = global::eventEngine->firstEvent(); if (_printEvents) { events::logAllEvents(e); } global::eventEngine->triggerActions(); global::eventEngine->triggerTopics(); global::eventEngine->postFrameCleanup(); global::memoryManager->PersistentMemory.housekeeping(); LTRACE("OpenSpaceEngine::postDraw(end)"); } void OpenSpaceEngine::keyboardCallback(Key key, KeyModifier mod, KeyAction action, IsGuiWindow isGuiWindow) { ZoneScoped; if (_loadingScreen) { // If the loading screen object exists, we are currently loading and want key // presses to behave differently if (key == Key::Escape) { _loadingScreen->abort(); } return; } // We need to do this check before the callback functions as we would otherwise // immediately cancel a shutdown if someone pressed the ESC key. Similar argument for // only checking for the Press action. Since the 'Press' of ESC will trigger the // shutdown, the 'Release' in some frame later would cancel it immediately again if (action == KeyAction::Press && _shutdown.inShutdown) { _shutdown.inShutdown = false; global::eventEngine->publishEvent( events::EventApplicationShutdown::State::Aborted ); return; } if (!global::configuration->isConsoleDisabled) { const bool isConsumed = global::luaConsole->keyboardCallback(key, mod, action); if (isConsumed) { return; } } for (const global::callback::KeyboardCallback& func : *global::callback::keyboard) { const bool isConsumed = func(key, mod, action, isGuiWindow); if (isConsumed) { return; } } global::navigationHandler->keyboardCallback(key, mod, action); if (!global::navigationHandler->disabledKeybindings()) { global::keybindingManager->keyboardCallback(key, mod, action); } global::interactionMonitor->markInteraction(); } void OpenSpaceEngine::charCallback(unsigned int codepoint, KeyModifier modifier, IsGuiWindow isGuiWindow) { ZoneScoped; for (const global::callback::CharacterCallback& func : *global::callback::character) { const bool isConsumed = func(codepoint, modifier, isGuiWindow); if (isConsumed) { return; } } global::luaConsole->charCallback(codepoint, modifier); global::interactionMonitor->markInteraction(); if (_shutdown.inShutdown) { _shutdown.inShutdown = false; global::eventEngine->publishEvent( events::EventApplicationShutdown::State::Aborted ); } } void OpenSpaceEngine::mouseButtonCallback(MouseButton button, MouseAction action, KeyModifier mods, IsGuiWindow isGuiWindow) { ZoneScoped; if (_disableAllMouseInputs) { return; } using F = global::callback::MouseButtonCallback; for (const F& func : *global::callback::mouseButton) { const bool isConsumed = func(button, action, mods, isGuiWindow); if (isConsumed) { // If the mouse was released, we still want to forward it to the navigation // handler in order to reliably terminate a rotation or zoom, or to the other // callbacks to for example release a drag and drop of a UI window. // Accidentally moving the cursor over a UI window is easy to miss and leads // to weird continuing movement if (action == MouseAction::Release) { continue; } else { return; } } } // Check if the user clicked on one of the 'buttons' the RenderEngine is drawing. // Only handle the clicks if we are in a GUI window if (action == MouseAction::Press && isGuiWindow) { const bool isConsumed = global::renderEngine->mouseActivationCallback(_mousePosition); if (isConsumed) { return; } } global::navigationHandler->mouseButtonCallback(button, action); global::interactionMonitor->markInteraction(); if (_shutdown.inShutdown) { _shutdown.inShutdown = false; global::eventEngine->publishEvent( events::EventApplicationShutdown::State::Aborted ); } } void OpenSpaceEngine::mousePositionCallback(double x, double y, IsGuiWindow isGuiWindow) { ZoneScoped; if (_disableAllMouseInputs) { return; } using F = global::callback::MousePositionCallback; for (const F& func : *global::callback::mousePosition) { func(x, y, isGuiWindow); } global::navigationHandler->mousePositionCallback(x, y); global::interactionMonitor->markInteraction(); _mousePosition = glm::vec2(static_cast(x), static_cast(y)); } void OpenSpaceEngine::mouseScrollWheelCallback(double posX, double posY, IsGuiWindow isGuiWindow) { ZoneScoped; if (_disableAllMouseInputs) { return; } using F = global::callback::MouseScrollWheelCallback; for (const F& func : *global::callback::mouseScrollWheel) { const bool isConsumed = func(posX, posY, isGuiWindow); if (isConsumed) { return; } } global::navigationHandler->mouseScrollWheelCallback(posY); global::interactionMonitor->markInteraction(); } void OpenSpaceEngine::touchDetectionCallback(TouchInput input) { ZoneScoped; for (const std::function& func : *global::callback::touchDetected) { const bool isConsumed = func(input); if (isConsumed) { return; } } } void OpenSpaceEngine::touchUpdateCallback(TouchInput input) { ZoneScoped; for (const std::function& func : *global::callback::touchUpdated) { const bool isConsumed = func(input); if (isConsumed) { return; } } } void OpenSpaceEngine::touchExitCallback(TouchInput input) { ZoneScoped; for (const std::function& func : *global::callback::touchExit) { func(input); } } void OpenSpaceEngine::handleDragDrop(std::filesystem::path file) { #ifdef WIN32 if (file.extension() == ".lnk") { LDEBUG(std::format("Replacing shell link path '{}'", file)); file = FileSys.resolveShellLink(file); } #endif // WIN32 const ghoul::lua::LuaState s; // This function will handle a specific file by providing the Lua script with // information about the dropped file and then executing the drag_drop handler. The // function returns whether the file was a valid drop target auto handleFile = [&s](std::filesystem::path f) -> bool { const std::filesystem::path path = absPath("${SCRIPTS}/drag_drop_handler.lua"); const std::string p = path.string(); int status = luaL_loadfile(s, p.c_str()); if (status != LUA_OK) { const std::string error = lua_tostring(s, -1); LERROR(error); return false; } #ifdef WIN32 if (f.extension() == ".lnk") { LDEBUG(std::format("Replacing shell link path '{}'", f)); f = FileSys.resolveShellLink(f); } #endif // WIN32 ghoul::lua::push(s, f); lua_setglobal(s, "filename"); std::filesystem::path basename = f.filename(); ghoul::lua::push(s, std::move(basename)); lua_setglobal(s, "basename"); std::string extension = f.extension().string(); extension = ghoul::toLowerCase(extension); ghoul::lua::push(s, extension); lua_setglobal(s, "extension"); int callStatus = lua_pcall(s, 0, 1, 0); if (callStatus != LUA_OK) { const std::string error = lua_tostring(s, -1); LERROR(error); } if (ghoul::lua::hasValue(s)) { std::string script = ghoul::lua::value(s); global::scriptEngine->queueScript(std::move(script)); lua_settop(s, 0); return true; } else { lua_settop(s, 0); return false; } }; if (std::filesystem::is_regular_file(file)) { // If we have a single file, we can just execute it directly const bool success = handleFile(file); if (!success) { LWARNING(std::format("Unhandled file dropped: {}", file)); } } else if (std::filesystem::is_directory(file)) { // If the file is a directory, we want to recursively get all files and handle // each of the files contained in the directory std::vector files = ghoul::filesystem::walkDirectory( file, ghoul::filesystem::Recursive::Yes, ghoul::filesystem::Sorted::No, [](const std::filesystem::path& f) { return std::filesystem::is_regular_file(f); } ); for (const std::filesystem::path& f : files) { // We ignore the return value here on purpose as we don't want to spam the log // with potential uninteresting messages about every file that was not a valid // target for a drag and drop operation handleFile(f); } } } std::vector OpenSpaceEngine::encode() { ZoneScoped; return global::syncEngine->encodeSyncables(); } void OpenSpaceEngine::decode(std::vector data) { ZoneScoped; global::syncEngine->decodeSyncables(std::move(data)); } properties::Property::Visibility OpenSpaceEngine::visibility() const { return static_cast(_visibility.value()); } void OpenSpaceEngine::toggleShutdownMode() { if (_shutdown.inShutdown) { // If we are already in shutdown mode, we want to disable it _shutdown.inShutdown = false; global::eventEngine->publishEvent( events::EventApplicationShutdown::State::Aborted ); } else { // Else, we have to enable it _shutdown.timer = _shutdown.waitTime; _shutdown.inShutdown = true; global::eventEngine->publishEvent( events::EventApplicationShutdown::State::Started ); } } OpenSpaceEngine::Mode OpenSpaceEngine::currentMode() const { return _currentMode; } bool OpenSpaceEngine::setMode(Mode newMode) { if (_currentMode == Mode::CameraPath && newMode == Mode::CameraPath) { // Special case: It is okay to trigger another camera path while one is // already playing. So just return that we were successful return true; } else if (newMode == _currentMode) { LERROR("Cannot switch to the currectly active mode"); return false; } else if (_currentMode != Mode::UserControl && newMode != Mode::UserControl) { LERROR(std::format( "Cannot switch to mode '{}' when in '{}' mode", stringify(newMode), stringify(_currentMode) )); return false; } LDEBUG(std::format("Mode: {}", stringify(newMode))); _currentMode = newMode; return true; } void OpenSpaceEngine::resetMode() { _currentMode = Mode::UserControl; LDEBUG(std::format("Reset engine mode to '{}'", stringify(_currentMode))); } OpenSpaceEngine::CallbackHandle OpenSpaceEngine::addModeChangeCallback( ModeChangeCallback cb) { const CallbackHandle handle = _nextCallbackHandle++; _modeChangeCallbacks.emplace_back(handle, std::move(cb)); return handle; } void OpenSpaceEngine::removeModeChangeCallback(CallbackHandle handle) { const auto it = std::find_if( _modeChangeCallbacks.begin(), _modeChangeCallbacks.end(), [handle](const std::pair& cb) { return cb.first == handle; } ); ghoul_assert( it != _modeChangeCallbacks.end(), "handle must be a valid callback handle" ); _modeChangeCallbacks.erase(it); } scripting::LuaLibrary OpenSpaceEngine::luaLibrary() { return { "", { codegen::lua::ToggleShutdown, codegen::lua::WriteDocumentation, codegen::lua::SetScreenshotFolder, codegen::lua::AddTag, codegen::lua::RemoveTag, codegen::lua::DownloadFile, codegen::lua::CreateSingleColorImage, codegen::lua::SaveBase64File, codegen::lua::IsMaster, codegen::lua::Version, codegen::lua::ReadCSVFile, codegen::lua::ResetCamera, codegen::lua::Configuration, codegen::lua::LayerServer, codegen::lua::LoadJson, codegen::lua::ResolveShortcut, codegen::lua::VramInUse, codegen::lua::RamInUse }, { absPath("${SCRIPTS}/core_scripts.lua") } }; } LoadingScreen* OpenSpaceEngine::loadingScreen() { return _loadingScreen.get(); } void OpenSpaceEngine::invalidatePropertyCache() { _isAllPropertiesCacheDirty = true; } void OpenSpaceEngine::invalidatePropertyOwnerCache() { _isAllPropertyOwnersCacheDirty = true; } const std::vector& OpenSpaceEngine::allProperties() const { if (_isAllPropertiesCacheDirty) { _allPropertiesCache = global::rootPropertyOwner->propertiesRecursive(); _isAllPropertiesCacheDirty = false; } return _allPropertiesCache; } const std::vector& OpenSpaceEngine::allPropertyOwners() const { if (_isAllPropertyOwnersCacheDirty) { _allPropertyOwnersCache = global::rootPropertyOwner->subownersRecursive(); _isAllPropertyOwnersCacheDirty = false; } return _allPropertyOwnersCache; } AssetManager& OpenSpaceEngine::assetManager() { ghoul_assert(_assetManager, "Asset Manager must not be nullptr"); return *_assetManager; } void setCameraFromProfile(const Profile& p) { if (!p.camera.has_value()) { // If the camera is not specified, we want to set it to a sensible default value interaction::NavigationState nav; nav.anchor = "Root"; nav.referenceFrame = "Root"; global::navigationHandler->setNavigationStateNextFrame(nav); return; } auto checkNodeExists = [](const std::string& node) { if (!global::renderEngine->scene()->sceneGraphNode(node)) { throw ghoul::RuntimeError(std::format( "Error when setting camera from profile. Could not find node '{}'", node )); } }; std::visit( overloaded { [&checkNodeExists](const Profile::CameraNavState& navStateProfile) { interaction::NavigationState nav; nav.anchor = navStateProfile.anchor; checkNodeExists(nav.anchor); if (navStateProfile.aim.has_value() && !(*navStateProfile.aim).empty()) { nav.aim = navStateProfile.aim.value(); checkNodeExists(nav.aim); } if (navStateProfile.referenceFrame.empty()) { nav.referenceFrame = nav.anchor; } else { nav.referenceFrame = navStateProfile.referenceFrame; checkNodeExists(navStateProfile.referenceFrame); } nav.position = navStateProfile.position; if (navStateProfile.up.has_value()) { nav.up = navStateProfile.up; } if (navStateProfile.yaw.has_value()) { nav.yaw = navStateProfile.yaw.value(); } if (navStateProfile.pitch.has_value()) { nav.pitch = navStateProfile.pitch.value(); } global::navigationHandler->setNavigationStateNextFrame(nav); }, [&checkNodeExists](const Profile::CameraGoToGeo& geo) { // Instead of direct calls to navigation state code, Lua commands with // globebrowsing goToGeo are used because this prevents a module // dependency in this core code. Eventually, goToGeo will be incorporated // in the OpenSpace core and this code will change. checkNodeExists(geo.anchor); std::string geoScript; if (geo.altitude.has_value()) { geoScript = std::format( "openspace.navigation.flyToGeo([[{}]], {}, {}, {}, 0)", geo.anchor, geo.latitude, geo.longitude, *geo.altitude ); } else { geoScript = std::format( "openspace.navigation.flyToGeo2([[{}]], {}, {}, false, 0)", geo.anchor, geo.latitude, geo.longitude ); } global::scriptEngine->queueScript(geoScript); }, [&checkNodeExists](const Profile::CameraGoToNode& node) { using namespace interaction; checkNodeExists(node.anchor); NodeCameraStateSpec spc = { .identifier = node.anchor, .height = node.height, .useTargetUpDirection = true }; global::navigationHandler->setCameraFromNodeSpecNextFrame(std::move(spc)); } }, p.camera.value() ); } void setModulesFromProfile(const Profile& p) { for (Profile::Module mod : p.modules) { const std::vector& m = global::moduleEngine->modules(); const auto it = std::find_if(m.begin(), m.end(), [&mod](const OpenSpaceModule* moduleSearch) { return (moduleSearch->identifier() == mod.name); }); if (it != m.end()) { if (mod.loadedInstruction.has_value()) { global::scriptEngine->queueScript(mod.loadedInstruction.value()); } } else { if (mod.notLoadedInstruction.has_value()) { global::scriptEngine->queueScript(mod.notLoadedInstruction.value()); } } } } void setActionsFromProfile(const Profile& p) { for (Profile::Action a : p.actions) { if (a.identifier.empty()) { LERROR("Identifier must to provided to register action"); } if (global::actionManager->hasAction(a.identifier)) { LERROR(std::format( "Action for identifier '{}' already existed & registered", a.identifier )); } if (a.script.empty()) { LERROR(std::format( "Identifier '{}' does not provide a Lua command to execute", a.identifier )); } interaction::Action action; action.identifier = a.identifier; action.command = a.script; action.name = a.name; action.documentation = a.documentation; action.guiPath = a.guiPath; action.isLocal = interaction::Action::IsLocal(a.isLocal); global::actionManager->registerAction(std::move(action)); } } void setKeybindingsFromProfile(const Profile& p) { for (Profile::Keybinding k : p.keybindings) { if (k.action.empty()) { LERROR("Action must not be empty"); } if (!global::actionManager->hasAction(k.action)) { LERROR(std::format("Action '{}' does not exist", k.action)); } if (k.key.key == openspace::Key::Unknown) { LERROR(std::format( "Could not find key '{}'", std::to_string(static_cast(k.key.key)) )); } global::keybindingManager->bindKey(k.key.key, k.key.modifier, k.action); } } void setMarkInterestingNodesFromProfile(const Profile& p) { for (const std::string& nodeName : p.markNodes) { SceneGraphNode* node = global::renderEngine->scene()->sceneGraphNode(nodeName); if (node) { node->addTag("GUI.Interesting"); } } } void setAdditionalScriptsFromProfile(const Profile& p) { for (const std::string& a : p.additionalScripts) { global::scriptEngine->queueScript(a); } } } // namespace openspace