From 0f9fbdbf635fdc1d8f28450b70fcb4e47bdb7a76 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 30 Jul 2023 00:40:05 -0500 Subject: [PATCH] Load placefile icons into texture atlas cache --- scwx-qt/scwx-qt.cmake | 5 +- scwx-qt/source/scwx/qt/external/stb_image.cpp | 4 + scwx-qt/source/scwx/qt/main/main.cpp | 1 + .../scwx/qt/manager/placefile_manager.cpp | 47 +++--- .../scwx/qt/manager/resource_manager.cpp | 9 +- .../scwx/qt/manager/resource_manager.hpp | 2 + scwx-qt/source/scwx/qt/util/network.cpp | 36 +++++ scwx-qt/source/scwx/qt/util/network.hpp | 26 ++++ scwx-qt/source/scwx/qt/util/texture_atlas.cpp | 135 +++++++++++++++--- scwx-qt/source/scwx/qt/util/texture_atlas.hpp | 1 + wxdata/include/scwx/gr/placefile.hpp | 2 + wxdata/source/scwx/gr/placefile.cpp | 13 ++ 12 files changed, 237 insertions(+), 44 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/external/stb_image.cpp create mode 100644 scwx-qt/source/scwx/qt/util/network.cpp create mode 100644 scwx-qt/source/scwx/qt/util/network.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index ebf8ca16..9a60ef01 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -46,7 +46,8 @@ set(HDR_CONFIG source/scwx/qt/config/county_database.hpp source/scwx/qt/config/radar_site.hpp) set(SRC_CONFIG source/scwx/qt/config/county_database.cpp source/scwx/qt/config/radar_site.cpp) -set(SRC_EXTERNAL source/scwx/qt/external/stb_rect_pack.cpp) +set(SRC_EXTERNAL source/scwx/qt/external/stb_image.cpp + source/scwx/qt/external/stb_rect_pack.cpp) set(HDR_GL source/scwx/qt/gl/gl.hpp source/scwx/qt/gl/gl_context.hpp source/scwx/qt/gl/shader_program.hpp @@ -203,6 +204,7 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/geographic_lib.hpp source/scwx/qt/util/json.hpp source/scwx/qt/util/maplibre.hpp + source/scwx/qt/util/network.hpp source/scwx/qt/util/streams.hpp source/scwx/qt/util/texture_atlas.hpp source/scwx/qt/util/q_file_buffer.hpp @@ -215,6 +217,7 @@ set(SRC_UTIL source/scwx/qt/util/color.cpp source/scwx/qt/util/geographic_lib.cpp source/scwx/qt/util/json.cpp source/scwx/qt/util/maplibre.cpp + source/scwx/qt/util/network.cpp source/scwx/qt/util/texture_atlas.cpp source/scwx/qt/util/q_file_buffer.cpp source/scwx/qt/util/q_file_input_stream.cpp diff --git a/scwx-qt/source/scwx/qt/external/stb_image.cpp b/scwx-qt/source/scwx/qt/external/stb_image.cpp new file mode 100644 index 00000000..c9598836 --- /dev/null +++ b/scwx-qt/source/scwx/qt/external/stb_image.cpp @@ -0,0 +1,4 @@ +#define STB_IMAGE_IMPLEMENTATION +#define STBI_ASSERT(x) +#define STBI_FAILURE_USERMSG +#include diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 8e9967b7..274c13ca 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp index 8793f9f9..1a378a18 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -35,7 +37,7 @@ public: explicit Impl(PlacefileManager* self) : self_ {self} {} ~Impl() {} - static std::string NormalizeUrl(const std::string& urlString); + static void LoadResources(const std::shared_ptr& placefile); boost::asio::thread_pool threadPool_ {1u}; @@ -67,7 +69,7 @@ public: void Update(); void UpdateAsync(); - void UpdatePlacefile(std::shared_ptr placefile); + void UpdatePlacefile(const std::shared_ptr& placefile); Impl* p; @@ -78,10 +80,6 @@ public: boost::asio::thread_pool threadPool_ {1u}; boost::asio::steady_timer refreshTimer_ {threadPool_}; std::mutex refreshMutex_ {}; - -signals: - void Updated(const std::string& name, - std::shared_ptr placefile); }; PlacefileManager::PlacefileManager() : p(std::make_unique(this)) {} @@ -167,7 +165,7 @@ void PlacefileManager::set_placefile_thresholded(const std::string& name, void PlacefileManager::set_placefile_url(const std::string& name, const std::string& newUrl) { - std::string normalizedUrl = Impl::NormalizeUrl(newUrl); + std::string normalizedUrl = util::network::NormalizeUrl(newUrl); std::unique_lock lock(p->placefileRecordLock_); @@ -235,7 +233,7 @@ PlacefileManager::GetActivePlacefiles() void PlacefileManager::AddUrl(const std::string& urlString) { - std::string normalizedUrl = Impl::NormalizeUrl(urlString); + std::string normalizedUrl = util::network::NormalizeUrl(urlString); std::unique_lock lock(p->placefileRecordLock_); @@ -318,6 +316,8 @@ void PlacefileManager::LoadFile(const std::string& filename) void PlacefileManager::Impl::PlacefileRecord::Update() { + logger_->debug("Update: {}", name_); + // Make a copy of name in the event it changes. const std::string name {name_}; @@ -399,6 +399,9 @@ void PlacefileManager::Impl::PlacefileRecord::Update() if (updatedPlacefile != nullptr) { + // Load placefile resources + Impl::LoadResources(updatedPlacefile); + // Check the name matches, in case the name updated if (name_ == name) { @@ -421,7 +424,7 @@ void PlacefileManager::Impl::PlacefileRecord::UpdateAsync() } void PlacefileManager::Impl::PlacefileRecord::UpdatePlacefile( - std::shared_ptr placefile) + const std::shared_ptr& placefile) { // Update placefile placefile_ = placefile; @@ -448,22 +451,22 @@ std::shared_ptr PlacefileManager::Instance() return placefileManager; } -std::string PlacefileManager::Impl::NormalizeUrl(const std::string& urlString) +void PlacefileManager::Impl::LoadResources( + const std::shared_ptr& placefile) { - std::string normalizedUrl; + const auto iconFiles = placefile->icon_files(); - // Normalize URL string - QUrl url = QUrl::fromUserInput(QString::fromStdString(urlString)); - if (url.isLocalFile()) - { - normalizedUrl = QDir::toNativeSeparators(url.toLocalFile()).toStdString(); - } - else - { - normalizedUrl = urlString; - } + const QUrl baseUrl = + QUrl::fromUserInput(QString::fromStdString(placefile->name())); - return normalizedUrl; + // TODO: Parallelize + for (auto& iconFile : iconFiles) + { + QUrl fileUrl = QUrl(QString::fromStdString(iconFile->filename_)); + QUrl resolvedUrl = baseUrl.resolved(fileUrl); + + ResourceManager::LoadImageResource(resolvedUrl.toString().toStdString()); + } } } // namespace manager diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index bbe06e88..b74de3e9 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -5,9 +5,8 @@ #include #include -#include - #include +#include namespace scwx { @@ -62,6 +61,12 @@ std::shared_ptr Font(types::Font font) return nullptr; } +void LoadImageResource(const std::string& urlString) +{ + util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); + textureAtlas.CacheTexture(urlString, urlString); +} + static void LoadFonts() { for (auto& fontName : fontNames_) diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp index 909373db..47df5461 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp @@ -18,6 +18,8 @@ void Shutdown(); int FontId(types::Font font); std::shared_ptr Font(types::Font font); +void LoadImageResource(const std::string& urlString); + } // namespace ResourceManager } // namespace manager } // namespace qt diff --git a/scwx-qt/source/scwx/qt/util/network.cpp b/scwx-qt/source/scwx/qt/util/network.cpp new file mode 100644 index 00000000..2e8d442f --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/network.cpp @@ -0,0 +1,36 @@ +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace network +{ + +std::string NormalizeUrl(const std::string& urlString) +{ + std::string normalizedUrl; + + // Normalize URL string + QUrl url = QUrl::fromUserInput(QString::fromStdString(urlString)); + if (url.isLocalFile()) + { + normalizedUrl = QDir::toNativeSeparators(url.toLocalFile()).toStdString(); + } + else + { + normalizedUrl = urlString; + } + + return normalizedUrl; +} + +} // namespace network +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/network.hpp b/scwx-qt/source/scwx/qt/util/network.hpp new file mode 100644 index 00000000..23142b3a --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/network.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace network +{ + +/** + * @brief Converts a local or remote URL to a consistent format. + * + * @param [in] urlString URL to normalize + * + * @return Normalized URL string + */ +std::string NormalizeUrl(const std::string& urlString); + +} // namespace network +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp index 9c5dcc04..864eeae8 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -13,8 +14,11 @@ #include #include +#include +#include #include #include +#include #if defined(_MSC_VER) # pragma warning(pop) @@ -48,6 +52,9 @@ public: std::unordered_map texturePathMap_; std::shared_mutex texturePathMutex_; + std::shared_mutex textureCacheMutex_; + std::unordered_map textureCache_; + boost::gil::rgba8_image_t atlas_; std::unordered_map atlasMap_; std::shared_mutex atlasMutex_; @@ -66,6 +73,34 @@ void TextureAtlas::RegisterTexture(const std::string& name, p->texturePathMap_.insert_or_assign(name, path); } +bool TextureAtlas::CacheTexture(const std::string& name, + const std::string& path) +{ + // If the image is already loaded, we don't need to load it again + { + std::shared_lock lock(p->textureCacheMutex_); + + if (p->textureCache_.contains(path)) + { + return false; + } + } + + // Attempt to load the image + boost::gil::rgba8_image_t image = TextureAtlas::Impl::LoadImage(path); + + // If the image is valid + if (image.width() > 0 && image.height() > 0) + { + // Store it in the texture cache + std::unique_lock lock(p->textureCacheMutex_); + + p->textureCache_.emplace(name, std::move(image)); + } + + return true; +} + void TextureAtlas::BuildAtlas(size_t width, size_t height) { logger_->debug("Building {}x{} texture atlas", width, height); @@ -110,6 +145,11 @@ void TextureAtlas::BuildAtlas(size_t width, size_t height) }); } + // TODO: Cached images + { + + } + // Pack images { logger_->trace("Packing {} images", images.size()); @@ -260,29 +300,86 @@ TextureAtlas::Impl::LoadImage(const std::string& imagePath) boost::gil::rgba8_image_t image; - QFile imageFile(imagePath.c_str()); + QUrl url = QUrl::fromUserInput(QString::fromStdString(imagePath)); - imageFile.open(QIODevice::ReadOnly); - - if (!imageFile.isOpen()) + if (url.isLocalFile()) { - logger_->error("Could not open image: {}", imagePath); - return image; + QFile imageFile(imagePath.c_str()); + + imageFile.open(QIODevice::ReadOnly); + + if (!imageFile.isOpen()) + { + logger_->error("Could not open image: {}", imagePath); + return image; + } + + boost::iostreams::stream dataStream(imageFile); + + try + { + boost::gil::read_and_convert_image( + dataStream, image, boost::gil::png_tag()); + } + catch (const std::exception& ex) + { + logger_->error("Error reading image: {}", ex.what()); + return image; + } } - - boost::iostreams::stream dataStream(imageFile); - - boost::gil::image x; - - try + else { - boost::gil::read_and_convert_image( - dataStream, image, boost::gil::png_tag()); - } - catch (const std::exception& ex) - { - logger_->error("Error reading image: {}", ex.what()); - return image; + auto response = cpr::Get(cpr::Url {imagePath}, network::cpr::GetHeader()); + + if (cpr::status::is_success(response.status_code)) + { + // Use stbi, since we can only guess the image format + static constexpr int desiredChannels = 4; + + int width; + int height; + int numChannels; + + unsigned char* pixelData = stbi_load_from_memory( + reinterpret_cast(response.text.data()), + static_cast( + std::clamp(response.text.size(), 0, INT32_MAX)), + &width, + &height, + &numChannels, + desiredChannels); + + if (pixelData == nullptr) + { + logger_->error("Error loading image: {}", stbi_failure_reason()); + return image; + } + + // Create a view pointing to the STB image data + auto imageView = boost::gil::interleaved_view( + width, + height, + reinterpret_cast(pixelData), + width * desiredChannels); + + // Copy the view to the destination image + image = boost::gil::rgba8_image_t(imageView); + + if (numChannels == 3) + { + // TODO: If no alpha channel, replace black with transparent + } + + stbi_image_free(pixelData); + } + else if (response.status_code == 0) + { + logger_->error("Error loading image: {}", response.error.message); + } + else + { + logger_->error("Error loading image: {}", response.status_line); + } } return image; diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp index bf904e6c..5bf07018 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp @@ -67,6 +67,7 @@ public: static TextureAtlas& Instance(); void RegisterTexture(const std::string& name, const std::string& path); + bool CacheTexture(const std::string& name, const std::string& path); void BuildAtlas(size_t width, size_t height); GLuint BufferAtlas(gl::OpenGLFunctions& gl); diff --git a/wxdata/include/scwx/gr/placefile.hpp b/wxdata/include/scwx/gr/placefile.hpp index d2a70c76..c6d391cd 100644 --- a/wxdata/include/scwx/gr/placefile.hpp +++ b/wxdata/include/scwx/gr/placefile.hpp @@ -109,6 +109,8 @@ public: */ std::vector> GetDrawItems(); + std::vector> icon_files(); + std::string name() const; std::string title() const; std::unordered_map> fonts(); diff --git a/wxdata/source/scwx/gr/placefile.cpp b/wxdata/source/scwx/gr/placefile.cpp index 10b9bedd..4ecb74c3 100644 --- a/wxdata/source/scwx/gr/placefile.cpp +++ b/wxdata/source/scwx/gr/placefile.cpp @@ -86,6 +86,19 @@ std::vector> Placefile::GetDrawItems() return p->drawItems_; } +std::vector> Placefile::icon_files() +{ + std::vector> iconFiles {}; + iconFiles.reserve(p->iconFiles_.size()); + + std::transform(p->iconFiles_.begin(), + p->iconFiles_.end(), + std::back_inserter(iconFiles), + [](auto& iconFile) { return iconFile.second; }); + + return iconFiles; +} + std::string Placefile::name() const { return p->name_;