diff --git a/data/web/default_ui_panels.json b/data/web/default_ui_panels.json index e964e1f23d..8721e18de1 100644 --- a/data/web/default_ui_panels.json +++ b/data/web/default_ui_panels.json @@ -102,6 +102,12 @@ "isOpen": false }, "17": { + "id": "assetsFolderPanel", + "visible": false, + "name": "Assets", + "isOpen": false + }, + "18": { "id": "devPanel", "visible": false, "name": "Dev Panel", diff --git a/include/openspace/engine/globals.h b/include/openspace/engine/globals.h index c85039cf88..380165f991 100644 --- a/include/openspace/engine/globals.h +++ b/include/openspace/engine/globals.h @@ -35,6 +35,7 @@ namespace openspace { struct Configuration; class Dashboard; class DeferredcasterManager; +class DownloadEventEngine; class DownloadManager; class EventEngine; class LuaConsole; @@ -72,6 +73,7 @@ namespace global { inline ghoul::fontrendering::FontManager* fontManager; inline Dashboard* dashboard; inline DeferredcasterManager* deferredcasterManager; +inline DownloadEventEngine* downloadEventEngine; inline DownloadManager* downloadManager; inline EventEngine* eventEngine; inline LuaConsole* luaConsole; diff --git a/include/openspace/events/event.h b/include/openspace/events/event.h index 563e4c5a44..012bb29936 100644 --- a/include/openspace/events/event.h +++ b/include/openspace/events/event.h @@ -60,7 +60,7 @@ struct Event { enum class Type : uint8_t { ParallelConnection, ProfileLoadingFinished, - AssetLoadingFinished, + AssetLoading, ApplicationShutdown, CameraFocusTransition, TimeOfInterestReached, @@ -152,17 +152,32 @@ struct EventProfileLoadingFinished : public Event { }; /** -* This event is created when the loading of all assets are finished. This is emitted -* regardless of whether it is the initial startup of a profile, or any subsequent asset -* being loaded e.g., through add or drag-and-drop. +* This event is created whenever the loading state of an assets changes. An asset can +* enter one of four states: `Loading`, `Loaded`, `Unloaded`, or `Error`. This event is +* emitted regardless of whether it is the initial startup of a profile, or any subsequent +* asset being added or revmoed e.g., through add or drag-and-drop. */ -struct EventAssetLoadingFinished : public Event { - static constexpr Type Type = Event::Type::AssetLoadingFinished; +struct EventAssetLoading : public Event { + static constexpr Type Type = Event::Type::AssetLoading; + + enum class State { + Loaded, + Loading, + Unloaded, + Error + }; /** - * Creates an instance of an AssetLoadingFinished event. + * Creates an instance of an AssetLoading event. + * + * \param assetPath_ The path to the asset + * \param state_ The new state of the asset given by 'asstPath_'; is one of `Loading`, + * `Loaded`, `Unloaded`, or `Error` */ - EventAssetLoadingFinished(); + EventAssetLoading(const std::filesystem::path& assetPath_, State newState); + + std::filesystem::path assetPath; + State state; }; /** diff --git a/include/openspace/scene/asset.h b/include/openspace/scene/asset.h index 89041e63cf..b4f34cfd48 100644 --- a/include/openspace/scene/asset.h +++ b/include/openspace/scene/asset.h @@ -207,6 +207,14 @@ public: */ bool hasInitializedParent() const; + /** + * Returns a list of the parents of this Asset that is currently in an initialized + * state, meaning any parent that is still interested in this Asset at all. + * + * \return A list of parent filepaths that are interested in this asset + */ + std::vector initializedParents() const; + /** * Deinitializes this Asset and recursively deinitializes the required assets if this * Asset was their ownly initialized parent. If the Asset was already deinitialized, diff --git a/include/openspace/util/downloadeventengine.h b/include/openspace/util/downloadeventengine.h new file mode 100644 index 0000000000..46c9406f8c --- /dev/null +++ b/include/openspace/util/downloadeventengine.h @@ -0,0 +1,72 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_SYNC___DOWNLOAD_EVENT_ENGINE___H__ +#define __OPENSPACE_MODULE_SYNC___DOWNLOAD_EVENT_ENGINE___H__ + +#include +#include +#include +#include +#include + +namespace openspace { + +// @TODO (anden88 2025-10-10): This class was specifically written for the multi-threaded +// url- and httpSynchronization events. In the future we should make this more general +// purposed. +class DownloadEventEngine { +public: + struct DownloadEvent { + enum class Type { + Started, + Progress, + Finished, + Failed + }; + + Type type; + std::string id; + int64_t downloadedBytes; + std::optional totalBytes; + }; + + using Callback = std::function; + + int subscribe(Callback cb); + void unsubscribe(int id); + + void publish(const DownloadEvent& event); + void publish(const std::string& id, DownloadEvent::Type type, + int64_t downloadedBytes = 0, std::optional totalBytes = std::nullopt); + +private: + std::mutex _mutex; + int _id = 0; + std::unordered_map _subscribers; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_SYNC___DOWNLOAD_EVENT_ENGINE___H__ diff --git a/modules/server/CMakeLists.txt b/modules/server/CMakeLists.txt index b10475ebf8..d422eda160 100644 --- a/modules/server/CMakeLists.txt +++ b/modules/server/CMakeLists.txt @@ -38,6 +38,7 @@ set(HEADER_FILES include/topics/camerapathtopic.h include/topics/cameratopic.h include/topics/documentationtopic.h + include/topics/downloadeventtopic.h include/topics/enginemodetopic.h include/topics/errorlogtopic.h include/topics/eventtopic.h @@ -70,6 +71,7 @@ set(SOURCE_FILES src/topics/camerapathtopic.cpp src/topics/cameratopic.cpp src/topics/documentationtopic.cpp + src/topics/downloadeventtopic.cpp src/topics/enginemodetopic.cpp src/topics/errorlogtopic.cpp src/topics/eventtopic.cpp diff --git a/modules/server/include/connection.h b/modules/server/include/connection.h index 5e6e2db9c7..63abaa03d2 100644 --- a/modules/server/include/connection.h +++ b/modules/server/include/connection.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -71,11 +72,10 @@ private: std::map> _topics; std::unique_ptr _socket; std::thread _thread; + std::mutex _mutex; std::string _address; bool _isAuthorized = false; - std::map _messageQueue; - std::map _sentMessages; }; } // namespace openspace diff --git a/modules/server/include/topics/downloadeventtopic.h b/modules/server/include/topics/downloadeventtopic.h new file mode 100644 index 0000000000..8b3b65dcd0 --- /dev/null +++ b/modules/server/include/topics/downloadeventtopic.h @@ -0,0 +1,49 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_SERVER___DOWNLOADEVENT_TOPIC___H__ +#define __OPENSPACE_MODULE_SERVER___DOWNLOADEVENT_TOPIC___H__ + +#include + +#include + +namespace openspace { + +class DownloadEventTopic : public Topic { +public: + ~DownloadEventTopic() override; + + void handleJson(const nlohmann::json& json) override; + bool isDone() const override; + +private: + bool _isSubscribedTo = false; + int _subscriptionID = -1; + std::unordered_map _lastCallBack; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_SERVER___DOWNLOADEVENT_TOPIC___H__ diff --git a/modules/server/src/connection.cpp b/modules/server/src/connection.cpp index 6dfc99363b..ff99ccf5ff 100644 --- a/modules/server/src/connection.cpp +++ b/modules/server/src/connection.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,7 @@ Connection::Connection(std::unique_ptr s, std::string address _topicFactory.registerClass("camera"); _topicFactory.registerClass("cameraPath"); _topicFactory.registerClass("documentation"); + _topicFactory.registerClass("downloadEvent"); _topicFactory.registerClass("engineMode"); _topicFactory.registerClass("errorLog"); _topicFactory.registerClass("event"); @@ -214,6 +216,7 @@ void Connection::handleJson(const nlohmann::json& json) { void Connection::sendMessage(const std::string& message) { ZoneScoped; + std::lock_guard lock(_mutex); _socket->putMessage(message); } diff --git a/modules/server/src/topics/downloadeventtopic.cpp b/modules/server/src/topics/downloadeventtopic.cpp new file mode 100644 index 0000000000..6a7dac6494 --- /dev/null +++ b/modules/server/src/topics/downloadeventtopic.cpp @@ -0,0 +1,90 @@ +/***************************************************************************************** + * * + * 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 + +namespace { + constexpr std::string_view StartSubscription = "start_subscription"; + constexpr std::string_view StopSubscription = "stop_subscription"; + constexpr std::chrono::milliseconds CallbackUpdateInterval(250); +} // namespace + +namespace openspace { + +DownloadEventTopic::~DownloadEventTopic() { + if (_isSubscribedTo) { + global::downloadEventEngine->unsubscribe(_subscriptionID); + _isSubscribedTo = false; + } +} + +bool DownloadEventTopic::isDone() const { + return !_isSubscribedTo; +} + +void DownloadEventTopic::handleJson(const nlohmann::json& json) { + const std::string& event = json.at("event").get(); + + if (event == StartSubscription) { + _isSubscribedTo = true; + + auto callback = [this](const DownloadEventEngine::DownloadEvent& event) { + // Limit how often we send data to frontend to reduce traffic + if (event.type == DownloadEventEngine::DownloadEvent::Type::Progress) { + const auto now = std::chrono::steady_clock::now(); + auto& last = _lastCallBack[event.id]; + + if (now - last >= CallbackUpdateInterval) { + last = now; + } + else { + return; + } + } + + nlohmann::json payload; + payload["type"] = event.type; + payload["id"] = event.id; + payload["downloadedBytes"] = event.downloadedBytes; + if (event.totalBytes.has_value()) { + payload["totalBytes"] = event.totalBytes.value(); + } + + _connection->sendJson(wrappedPayload(payload)); + }; + _subscriptionID = global::downloadEventEngine->subscribe(callback); + } + + else if (event == StopSubscription) { + global::downloadEventEngine->unsubscribe(_subscriptionID); + _isSubscribedTo = false; + } +} + +} // namespace openspace diff --git a/modules/sync/syncs/httpsynchronization.cpp b/modules/sync/syncs/httpsynchronization.cpp index 14490592a1..0481b028f0 100644 --- a/modules/sync/syncs/httpsynchronization.cpp +++ b/modules/sync/syncs/httpsynchronization.cpp @@ -26,6 +26,8 @@ #include #include +#include +#include #include #include #include @@ -332,9 +334,25 @@ HttpSynchronization::trySyncFromUrl(std::string url) { _nSynchronizedBytes += sd.second.downloadedBytes; } + DownloadEventEngine::DownloadEvent event = { + .type = DownloadEventEngine::DownloadEvent::Type::Progress, + .id = line, + .downloadedBytes = downloadedBytes, + .totalBytes = totalBytes + }; + global::downloadEventEngine->publish(event); + return !_shouldCancel; }); + DownloadEventEngine::DownloadEvent event = { + .type = DownloadEventEngine::DownloadEvent::Type::Started, + .id = line, + .downloadedBytes = 0 + }; + global::downloadEventEngine->publish(event); + LDEBUG(std::format("Started downloading '{}'", dl->url())); + dl->start(); } startedAllDownloads = true; @@ -373,6 +391,12 @@ HttpSynchronization::trySyncFromUrl(std::string url) { if (!d->hasSucceeded()) { LERROR(std::format("Error downloading file from URL '{}'", d->url())); failed = true; + + global::downloadEventEngine->publish( + d->url(), + DownloadEventEngine::DownloadEvent::Type::Failed + ); + LERROR(std::format("Failed to download '{}'", d->url())); continue; } @@ -421,16 +445,38 @@ HttpSynchronization::trySyncFromUrl(std::string url) { std::filesystem::remove(source); } } - if (failed) { - for (const std::unique_ptr& d : downloads) { - // Store all files that were synced to the ossync + + for (const std::unique_ptr& d : downloads) { + if (failed) { + // At least one download failed, (some downloads may have succeeded) if (d->hasSucceeded()) { _newSyncedFiles.push_back(d->url()); + global::downloadEventEngine->publish( + d->url(), + DownloadEventEngine::DownloadEvent::Type::Finished + ); + LDEBUG(std::format("Finished downloading '{}'", d->url())); + } + else { + global::downloadEventEngine->publish( + d->url(), + DownloadEventEngine::DownloadEvent::Type::Failed + ); + LERROR(std::format("Failed to download '{}'", d->url())); } } - return SynchronizationState::FileDownloadFail; + else { + // All downloads are successful + global::downloadEventEngine->publish( + d->url(), + DownloadEventEngine::DownloadEvent::Type::Finished + ); + LDEBUG(std::format("Finished downloading '{}'", d->url())); + } + } - return SynchronizationState::Success; + + return failed ? SynchronizationState::FileDownloadFail : SynchronizationState::Success; } } // namespace openspace diff --git a/modules/sync/syncs/urlsynchronization.cpp b/modules/sync/syncs/urlsynchronization.cpp index 885f9b29d6..32d9dd81a8 100644 --- a/modules/sync/syncs/urlsynchronization.cpp +++ b/modules/sync/syncs/urlsynchronization.cpp @@ -26,6 +26,8 @@ #include #include +#include +#include #include #include #include @@ -354,9 +356,25 @@ bool UrlSynchronization::trySyncUrls() { _nSynchronizedBytes += sd.second.downloadedBytes; } + DownloadEventEngine::DownloadEvent event = { + .type = DownloadEventEngine::DownloadEvent::Type::Progress, + .id = url, + .downloadedBytes = downloadedBytes, + .totalBytes = totalBytes + }; + global::downloadEventEngine->publish(event); + return !_shouldCancel; }); + DownloadEventEngine::DownloadEvent event = { + .type = DownloadEventEngine::DownloadEvent::Type::Started, + .id = url, + .downloadedBytes = 0 + }; + global::downloadEventEngine->publish(event); + LDEBUG(std::format("Started downloading '{}'", dl->url())); + dl->start(); } @@ -368,6 +386,11 @@ bool UrlSynchronization::trySyncUrls() { if (!d->hasSucceeded()) { failed = true; LERROR(std::format("Error downloading file from URL: {}", d->url())); + global::downloadEventEngine->publish( + d->url(), + DownloadEventEngine::DownloadEvent::Type::Failed + ); + LERROR(std::format("Failed to download '{}'", d->url())); continue; } @@ -394,7 +417,18 @@ bool UrlSynchronization::trySyncUrls() { ); failed = true; + global::downloadEventEngine->publish( + d->url(), + DownloadEventEngine::DownloadEvent::Type::Failed + ); + LERROR(std::format("Failed to download '{}'", d->url())); } + + global::downloadEventEngine->publish( + d->url(), + DownloadEventEngine::DownloadEvent::Type::Finished + ); + LDEBUG(std::format("Finished downloading '{}'", d->url())); } return !failed; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3bb78ce255..0687a2d792 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -174,6 +174,7 @@ set(OPENSPACE_SOURCE util/collisionhelper.cpp util/coordinateconversion.cpp util/distanceconversion.cpp + util/downloadeventengine.cpp util/dynamicfilesequencedownloader.cpp util/ellipsoid.cpp util/factorymanager.cpp @@ -372,6 +373,7 @@ set(OPENSPACE_HEADER ${PROJECT_SOURCE_DIR}/include/openspace/util/coordinateconversion.h ${PROJECT_SOURCE_DIR}/include/openspace/util/distanceconstants.h ${PROJECT_SOURCE_DIR}/include/openspace/util/distanceconversion.h + ${PROJECT_SOURCE_DIR}/include/openspace/util/downloadeventengine.h ${PROJECT_SOURCE_DIR}/include/openspace/util/dynamicfilesequencedownloader.h ${PROJECT_SOURCE_DIR}/include/openspace/util/ellipsoid.h ${PROJECT_SOURCE_DIR}/include/openspace/util/factorymanager.h diff --git a/src/engine/globals.cpp b/src/engine/globals.cpp index 4ad57fce5a..dca1a827a4 100644 --- a/src/engine/globals.cpp +++ b/src/engine/globals.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +76,7 @@ namespace { #ifdef WIN32 constexpr int TotalSize = sizeof(MemoryManager) + + sizeof(DownloadEventEngine) + sizeof(EventEngine) + sizeof(ghoul::fontrendering::FontManager) + sizeof(Dashboard) + @@ -141,6 +143,14 @@ void create() { openSpaceEngine = new OpenSpaceEngine; #endif // WIN32 +#ifdef WIN32 + downloadEventEngine = new (currentPos) DownloadEventEngine; + ghoul_assert(downloadEventEngine, "No downloadEventEngine"); + currentPos += sizeof(DownloadEventEngine); +#else // ^^^^ WIN32 / !WIN32 vvv + downloadEventEngine = new DownloadEventEngine; +#endif // WIN32 + #ifdef WIN32 eventEngine = new (currentPos) EventEngine; ghoul_assert(eventEngine, "No eventEngine"); @@ -626,6 +636,13 @@ void destroy() { delete fontManager; #endif // WIN32 + LDEBUGC("Globals", "Destroying 'DownloadEventEngine'"); +#ifdef WIN32 + downloadEventEngine->~DownloadEventEngine(); +#else // ^^^ WIN32 / !WIN32 vvv + delete downloadEventEngine; +#endif // WIN32 + LDEBUGC("Globals", "Destroying 'EventEngine'"); #ifdef WIN32 eventEngine->~EventEngine(); diff --git a/src/events/event.cpp b/src/events/event.cpp index 0cf72be3a6..e897f328d7 100644 --- a/src/events/event.cpp +++ b/src/events/event.cpp @@ -62,9 +62,18 @@ void log(int i, [[maybe_unused]] const EventProfileLoadingFinished& e) { LINFO(std::format("[{}] ProfileLoadingFinished", i)); } -void log(int i, const EventAssetLoadingFinished& e) { - ghoul_assert(e.type == EventAssetLoadingFinished::Type, "Wrong type"); - LINFO(std::format("[{}] AssetLoadingFinished", i)); +void log(int i, const EventAssetLoading& e) { + ghoul_assert(e.type == EventAssetLoading::Type, "Wrong type"); + std::string_view state = [](EventAssetLoading::State s) { + switch (s) { + case EventAssetLoading::State::Loaded: return "Loaded"; + case EventAssetLoading::State::Loading: return "Loading"; + case EventAssetLoading::State::Unloaded: return "Unloaded"; + case EventAssetLoading::State::Error: return "Error"; + default: throw ghoul::MissingCaseException(); + } + }(e.state); + LINFO(std::format("[{}] AssetLoading: '{}': ({})", i, e.assetPath, state)); } void log(int i, const EventApplicationShutdown& e) { @@ -235,7 +244,7 @@ std::string_view toString(Event::Type type) { switch (type) { case Event::Type::ParallelConnection: return "ParallelConnection"; case Event::Type::ProfileLoadingFinished: return "ProfileLoadingFinished"; - case Event::Type::AssetLoadingFinished: return "AssetLoadingFinished"; + case Event::Type::AssetLoading: return "AssetLoading"; case Event::Type::ApplicationShutdown: return "ApplicationShutdown"; case Event::Type::CameraFocusTransition: return "CameraFocusTransition"; case Event::Type::TimeOfInterestReached: return "TimeOfInterestReached"; @@ -271,8 +280,8 @@ Event::Type fromString(std::string_view str) { else if (str == "ProfileLoadingFinished") { return Event::Type::ProfileLoadingFinished; } - else if (str == "AssetLoadingFinished") { - return Event::Type::AssetLoadingFinished; + else if (str == "AssetLoading") { + return Event::Type::AssetLoading; } else if (str == "ApplicationShutdown") { return Event::Type::ApplicationShutdown; @@ -366,6 +375,26 @@ ghoul::Dictionary toParameter(const Event& e) { break; } break; + case Event::Type::AssetLoading: + d.setValue( + "AssetPath", + static_cast(e).assetPath + ); + switch (static_cast(e).state) { + case EventAssetLoading::State::Loaded: + d.setValue("State", "Loaded"s); + break; + case EventAssetLoading::State::Loading: + d.setValue("State", "Loading"s); + break; + case EventAssetLoading::State::Unloaded: + d.setValue("State", "Unloaded"s); + break; + case EventAssetLoading::State::Error: + d.setValue("State", "Error"s); + break; + } + break; case Event::Type::ApplicationShutdown: switch (static_cast(e).state) { case EventApplicationShutdown::State::Started: @@ -544,8 +573,8 @@ void logAllEvents(const Event* e) { case Event::Type::ProfileLoadingFinished: log(i, *static_cast(e)); break; - case Event::Type::AssetLoadingFinished: - log(i, *static_cast(e)); + case Event::Type::AssetLoading: + log(i, *static_cast(e)); break; case Event::Type::ApplicationShutdown: log(i, *static_cast(e)); @@ -634,8 +663,11 @@ EventProfileLoadingFinished::EventProfileLoadingFinished() : Event(Type) {} -EventAssetLoadingFinished::EventAssetLoadingFinished() +EventAssetLoading::EventAssetLoading(const std::filesystem::path& assetPath_, + State newState) : Event(Type) + , assetPath(assetPath_) + , state(newState) {} EventApplicationShutdown::EventApplicationShutdown(State state_) diff --git a/src/rendering/luaconsole.cpp b/src/rendering/luaconsole.cpp index a9896d847a..aa6686822a 100644 --- a/src/rendering/luaconsole.cpp +++ b/src/rendering/luaconsole.cpp @@ -1129,20 +1129,10 @@ bool LuaConsole::gatherPathSuggestions(size_t contextStart) { ghoul::filesystem::Sorted::Yes ); - auto containsNonAscii = [](const std::filesystem::path& p) { - const std::u8string s = p.generic_u8string(); - for (auto it = s.rbegin(); it != s.rend(); it++) { - if (static_cast(*it) > 0x7F) { - return true; - } - } - return false; - }; - std::vector entries; for (const std::filesystem::path& entry : suggestions) { // Filter paths that contain non-ASCII characters - if (containsNonAscii(entry)) { + if (ghoul::containsNonAscii(entry)) { continue; } diff --git a/src/scene/asset.cpp b/src/scene/asset.cpp index c2f901d3fc..476c398461 100644 --- a/src/scene/asset.cpp +++ b/src/scene/asset.cpp @@ -25,6 +25,9 @@ #include #include +#include +#include +#include #include #include #include @@ -184,6 +187,16 @@ bool Asset::hasInitializedParent() const { ); } +std::vector Asset::initializedParents() const { + std::vector parents; + for (const Asset* parent : _parentAssets) { + if (parent->isInitialized()) { + parents.push_back(parent->path()); + } + } + return parents; +} + bool Asset::isInitialized() const { return _state == State::Initialized; } @@ -284,6 +297,10 @@ void Asset::initialize() { } LDEBUG(std::format("Initializing asset '{}'", _assetPath)); + global::eventEngine->publishEvent( + _assetPath.string(), + events::EventAssetLoading::State::Loading + ); // 1. Initialize requirements for (Asset* child : _requiredAssets) { child->initialize(); @@ -308,6 +325,10 @@ void Asset::initialize() { // 3. Update state setState(State::Initialized); + global::eventEngine->publishEvent( + _assetPath.string(), + events::EventAssetLoading::State::Loaded + ); } void Asset::deinitialize() { diff --git a/src/scene/assetmanager.cpp b/src/scene/assetmanager.cpp index 723ba68f45..d9d91bb57b 100644 --- a/src/scene/assetmanager.cpp +++ b/src/scene/assetmanager.cpp @@ -230,10 +230,6 @@ void AssetManager::runAddQueue() { void AssetManager::update() { ZoneScoped; - - // Flag to keep track of when to emit synchronization event - const bool isLoadingAssets = !_toBeInitialized.empty(); - // Delete all the assets that have been marked for deletion in the previous frame { ZoneScopedN("Deleting assets"); @@ -303,11 +299,6 @@ void AssetManager::update() { it++; } } - - // If the _toBeInitialized state has changed in this update call we emit the event - if (isLoadingAssets && _toBeInitialized.empty()) { - global::eventEngine->publishEvent(); - } } void AssetManager::add(const std::string& path) { @@ -387,6 +378,10 @@ bool AssetManager::loadAsset(Asset* asset, Asset* parent) { } catch (const ghoul::lua::LuaRuntimeException& e) { LERROR(std::format("Could not load asset '{}': {}", asset->path(), e.message)); + global::eventEngine->publishEvent( + asset->path().string(), + events::EventAssetLoading::State::Error + ); return false; } catch (const ghoul::RuntimeError& e) { @@ -466,6 +461,10 @@ void AssetManager::unloadAsset(Asset* asset) { // might be painful _toBeDeleted.push_back(std::move(*it)); _assets.erase(it); + global::eventEngine->publishEvent( + asset->path().string(), + events::EventAssetLoading::State::Unloaded + ); } } @@ -964,6 +963,10 @@ void AssetManager::callOnInitialize(Asset* asset) const { for (const int init : it->second) { lua_rawgeti(*_luaState, LUA_REGISTRYINDEX, init); if (lua_pcall(*_luaState, 0, 0, 0) != LUA_OK) { + global::eventEngine->publishEvent( + asset->path().string(), + events::EventAssetLoading::State::Error + ); throw ghoul::lua::LuaRuntimeException(std::format( "When initializing '{}': {}", asset->path(), @@ -1058,7 +1061,8 @@ scripting::LuaLibrary AssetManager::luaLibrary() { codegen::lua::RemoveAll, codegen::lua::IsLoaded, codegen::lua::AllAssets, - codegen::lua::RootAssets + codegen::lua::RootAssets, + codegen::lua::Parents } }; } diff --git a/src/scene/assetmanager_lua.inl b/src/scene/assetmanager_lua.inl index 58c2d96cdf..7c01343b7d 100644 --- a/src/scene/assetmanager_lua.inl +++ b/src/scene/assetmanager_lua.inl @@ -108,6 +108,21 @@ namespace { return res; } +/** + * Returns the path to all parents that are still interested in this Asset e.g., through + * 'asset.require()' + */ +[[codegen::luawrap]] std::vector parents(std::string assetName) { + using namespace openspace; + std::vector as = global::openSpaceEngine->assetManager().allAssets(); + for (const Asset* a : as) { + if (a->path() == assetName) { + return a->initializedParents(); + } + } + return std::vector(); +} + #include "assetmanager_lua_codegen.cpp" } // namespace diff --git a/src/util/downloadeventengine.cpp b/src/util/downloadeventengine.cpp new file mode 100644 index 0000000000..45773f292b --- /dev/null +++ b/src/util/downloadeventengine.cpp @@ -0,0 +1,62 @@ +/***************************************************************************************** + * * + * 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 + +namespace openspace { + +int DownloadEventEngine::subscribe(Callback cb) { + std::lock_guard lock(_mutex); + int id = _id++; + _subscribers[id] = std::move(cb); + return id; +} + +void DownloadEventEngine::unsubscribe(int id) { + std::lock_guard lock(_mutex); + _subscribers.erase(id); +} + +void DownloadEventEngine::publish(const DownloadEvent& event) { + std::lock_guard lock(_mutex); + for (auto& [_, callback] : _subscribers) { + callback(event); + } +} + +void DownloadEventEngine::publish(const std::string& id, DownloadEvent::Type type, + int64_t downloadedBytes, + std::optional totalBytes) +{ + const DownloadEvent event = { + .type = type, + .id = id, + .downloadedBytes = downloadedBytes, + .totalBytes = totalBytes + }; + + publish(event); +} + +} // namespace openspace