diff --git a/src/core/fullscreenui_widgets.cpp b/src/core/fullscreenui_widgets.cpp index eee2d7979..f54d85c04 100644 --- a/src/core/fullscreenui_widgets.cpp +++ b/src/core/fullscreenui_widgets.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "fullscreenui_widgets.h" @@ -13,10 +13,12 @@ #include "video_thread.h" #include "util/gpu_device.h" +#include "util/http_cache.h" #include "util/image.h" #include "util/imgui_animated.h" #include "util/imgui_manager.h" #include "util/input_manager.h" +#include "util/object_archive.h" #include "util/shadergen.h" #include "common/assert.h" @@ -73,7 +75,16 @@ enum class SplitWindowFocusChange : u8 }; static std::optional LoadTextureImage(std::string_view path, u32 svg_width, u32 svg_height); +static std::optional LoadTextureImage(std::string_view filename, std::span buffer, u32 svg_width, + u32 svg_height); static std::shared_ptr UploadTexture(std::string_view path, const Image& image); +static void QueueTextureUploadFromBuffer(std::string_view filename, const std::span& buffer, + std::string&& insert_name, u32 svg_width, u32 svg_height, + bool use_task_for_decode); +static std::shared_ptr LoadTexture(std::string_view path, std::string_view name, u32 svg_width, + u32 svg_height); +static GPUTexture* LookupCachedTextureAsync(std::string_view path, std::string_view name, u32 svg_width, + u32 svg_height); static bool CompilePipelines(Error* error); @@ -673,6 +684,34 @@ std::optional FullscreenUI::LoadTextureImage(std::string_view path, u32 s return image; } +std::optional FullscreenUI::LoadTextureImage(std::string_view filename, std::span buffer, + u32 svg_width, u32 svg_height) +{ + std::optional image; + Error error; + + if (StringUtil::EqualNoCase(Path::GetExtension(filename), "svg")) + { + image = Image(); + if (!image->RasterizeSVG(buffer, svg_width, svg_height, &error)) + { + ERROR_LOG("Failed to rasterize SVG texture file '{}': {}", filename, error.GetDescription()); + image.reset(); + } + } + else + { + image = Image(); + if (!image->LoadFromBuffer(filename, buffer, &error)) + { + ERROR_LOG("Failed to read texture resource '{}': {}", filename, error.GetDescription()); + image.reset(); + } + } + + return image; +} + std::shared_ptr FullscreenUI::UploadTexture(std::string_view path, const Image& image) { Error error; @@ -688,9 +727,87 @@ std::shared_ptr FullscreenUI::UploadTexture(std::string_view path, c return std::shared_ptr(texture.release(), GPUDevice::PooledTextureDeleter()); } -std::shared_ptr FullscreenUI::LoadTexture(std::string_view path, u32 width_hint, u32 height_hint) +void FullscreenUI::QueueTextureUploadFromBuffer(std::string_view filename, const std::span& buffer, + std::string&& insert_name, u32 svg_width, u32 svg_height, + bool use_task_for_decode) { - std::optional image(LoadTextureImage(path, width_hint, height_hint)); + if (buffer.empty()) + { + ERROR_LOG("No data returned for {}", insert_name); + return; + } + + if (use_task_for_decode) + { + Host::QueueAsyncTask([filename = std::string(filename), buffer = DynamicHeapArray(buffer), + insert_name = std::move(insert_name), svg_width, svg_height]() mutable { + QueueTextureUploadFromBuffer(filename, buffer, std::move(insert_name), svg_width, svg_height, false); + }); + return; + } + + std::optional image = LoadTextureImage(filename, buffer, svg_width, svg_height); + if (!image.has_value()) + return; + + const std::lock_guard lock(s_state.shared_state_mutex); + s_state.texture_upload_queue.emplace_back(std::move(insert_name), std::move(image.value())); +} + +std::shared_ptr FullscreenUI::LoadTexture(std::string_view path, std::string_view name, u32 svg_width, + u32 svg_height) +{ + if (HTTPCache::IsHTTPURL(path)) + { + const std::string_view filename = HTTPCache::GetURLFilename(path); + const std::string_view& insert_name = name.empty() ? path : name; + + // avoid constructing callback until it's needed + Error error; + HTTPCache::LookupResult result = HTTPCache::Lookup(path, &error); + if (result.status() == HTTPCache::LookupStatus::Miss) + { + result = HTTPCache::LookupOrFetch(path, &error, + [filename = std::string(filename), insert_name = std::string(insert_name), + svg_width, svg_height](std::span data) mutable { + QueueTextureUploadFromBuffer(filename, data, std::move(insert_name), + svg_width, svg_height, true); + }); + } + + switch (result.status()) + { + case HTTPCache::LookupStatus::Hit: + { + const std::optional image = LoadTextureImage(filename, result.value().cspan(), svg_width, svg_height); + if (image.has_value()) + { + std::shared_ptr ret = UploadTexture(path, image.value()); + if (ret) + return ret; + } + } + break; + + case HTTPCache::LookupStatus::Miss: + { + WARNING_LOG("Cache miss when trying to synchronously load texture from URL '{}'.", path); + } + break; + + case HTTPCache::LookupStatus::Error: + { + ERROR_LOG("Failed to load URL from cache '{}': {}", path, error.GetDescription()); + } + break; + + DefaultCaseIsUnreachable(); + } + + return s_state.placeholder_texture; + } + + std::optional image(LoadTextureImage(path, svg_width, svg_height)); if (image.has_value()) { std::shared_ptr ret(UploadTexture(path, image.value())); @@ -701,6 +818,29 @@ std::shared_ptr FullscreenUI::LoadTexture(std::string_view path, u32 return s_state.placeholder_texture; } +std::shared_ptr FullscreenUI::LoadTexture(std::string_view name) +{ + return LoadTexture(name, {}, 0, 0); +} + +std::shared_ptr FullscreenUI::LoadTexture(std::string_view path, std::string_view name) +{ + return LoadTexture(path, name, 0, 0); +} + +std::shared_ptr FullscreenUI::LoadTexture(std::string_view name, u32 svg_width, u32 svg_height) +{ + // ignore size hints if it's not needed, don't duplicate + if (!TextureNeedsSVGDimensions(name)) + return LoadTexture(name, name, 0, 0); + + svg_width = static_cast(std::ceil(LayoutScale(static_cast(svg_width)))); + svg_height = static_cast(std::ceil(LayoutScale(static_cast(svg_height)))); + + const SmallString wh_name = SmallString::from_format("{}#{}x{}", name, svg_width, svg_height); + return LoadTexture(name, wh_name, svg_width, svg_height); +} + GPUTexture* FullscreenUI::FindCachedTexture(std::string_view name) { std::shared_ptr* tex_ptr = s_state.texture_cache.Lookup(name); @@ -722,11 +862,16 @@ GPUTexture* FullscreenUI::FindCachedTexture(std::string_view name, u32 svg_width } GPUTexture* FullscreenUI::GetCachedTexture(std::string_view name) +{ + return GetCachedTexture(name, name); +} + +GPUTexture* FullscreenUI::GetCachedTexture(std::string_view path, std::string_view name) { std::shared_ptr* tex_ptr = s_state.texture_cache.Lookup(name); if (!tex_ptr) { - std::shared_ptr tex = LoadTexture(name); + std::shared_ptr tex = LoadTexture(path); tex_ptr = s_state.texture_cache.Insert(std::string(name), std::move(tex)); } @@ -753,60 +898,85 @@ GPUTexture* FullscreenUI::GetCachedTexture(std::string_view name, u32 svg_width, return tex_ptr->get(); } -GPUTexture* FullscreenUI::GetCachedTextureAsync(std::string_view name) +GPUTexture* FullscreenUI::LookupCachedTextureAsync(std::string_view path, std::string_view name, u32 svg_width, + u32 svg_height) { - std::shared_ptr* tex_ptr = s_state.texture_cache.Lookup(name); - if (!tex_ptr) - { - // insert the placeholder - tex_ptr = s_state.texture_cache.Insert(std::string(name), s_state.placeholder_texture); + const std::string_view lookup_name = name.empty() ? path : name; + std::shared_ptr* tex_ptr = s_state.texture_cache.Lookup(lookup_name); + if (tex_ptr) + return tex_ptr->get(); + + // insert the placeholder + tex_ptr = s_state.texture_cache.Insert(std::string(lookup_name), s_state.placeholder_texture); - // queue the actual load - Host::QueueAsyncTask([path = std::string(name)]() mutable { - std::optional image(LoadTextureImage(path.c_str(), 0, 0)); + // queue load + Host::QueueAsyncTask([path = std::string(path), name = std::string(name), svg_width, svg_height]() mutable { + std::string& insert_name = name.empty() ? path : name; + if (HTTPCache::IsHTTPURL(path)) + { + const std::string_view filename = HTTPCache::GetURLFilename(path); + + // avoid constructing callback until it's needed + Error error; + const HTTPCache::LookupResult result = HTTPCache::Lookup(path, &error); + if (result.status() == HTTPCache::LookupStatus::Miss) + { + HTTPCache::LookupOrFetch(path, &error, + [filename = std::string(filename), insert_name, svg_width, + svg_height](const std::span& data) mutable { + QueueTextureUploadFromBuffer(filename, data, std::move(insert_name), svg_width, + svg_height, true); + }); + } + + // callback won't be executed on hit + if (result.status() == HTTPCache::LookupStatus::Hit) + { + QueueTextureUploadFromBuffer(filename, result.value().cspan(), std::move(insert_name), svg_width, svg_height, + false); + } + else if (result.status() == HTTPCache::LookupStatus::Error) + { + ERROR_LOG("Failed to lookup texture for URL '{}': {}", path, error.GetDescription()); + } + } + else + { + std::optional image = LoadTextureImage(path, svg_width, svg_height); // don't bother queuing back if it doesn't exist if (!image.has_value()) return; - std::unique_lock lock(s_state.shared_state_mutex); - s_state.texture_upload_queue.emplace_back(std::move(path), std::move(image.value())); - }); - } + const std::lock_guard lock(s_state.shared_state_mutex); + s_state.texture_upload_queue.emplace_back(std::move(insert_name), std::move(image.value())); + } + }); return tex_ptr->get(); } +GPUTexture* FullscreenUI::GetCachedTextureAsync(std::string_view name) +{ + return LookupCachedTextureAsync(name, {}, 0, 0); +} + +GPUTexture* FullscreenUI::GetCachedTextureAsync(std::string_view path, std::string_view name) +{ + return LookupCachedTextureAsync(path, name, 0, 0); +} + GPUTexture* FullscreenUI::GetCachedTextureAsync(std::string_view name, u32 svg_width, u32 svg_height) { // ignore size hints if it's not needed, don't duplicate if (!TextureNeedsSVGDimensions(name)) - return GetCachedTextureAsync(name); + return LookupCachedTextureAsync(name, {}, 0, 0); svg_width = static_cast(std::ceil(LayoutScale(static_cast(svg_width)))); svg_height = static_cast(std::ceil(LayoutScale(static_cast(svg_height)))); const SmallString wh_name = SmallString::from_format("{}#{}x{}", name, svg_width, svg_height); - std::shared_ptr* tex_ptr = s_state.texture_cache.Lookup(wh_name.view()); - if (!tex_ptr) - { - // insert the placeholder - tex_ptr = s_state.texture_cache.Insert(std::string(wh_name), s_state.placeholder_texture); - - // queue the actual load - Host::QueueAsyncTask([path = std::string(name), wh_name = std::string(wh_name), svg_width, svg_height]() mutable { - std::optional image(LoadTextureImage(path.c_str(), svg_width, svg_height)); - - // don't bother queuing back if it doesn't exist - if (!image.has_value()) - return; - - std::unique_lock lock(s_state.shared_state_mutex); - s_state.texture_upload_queue.emplace_back(std::move(wh_name), std::move(image.value())); - }); - } - - return tex_ptr->get(); + return LookupCachedTextureAsync(name, wh_name.view(), svg_width, svg_height); } bool FullscreenUI::InvalidateCachedTexture(std::string_view path) @@ -1293,7 +1463,6 @@ void FullscreenUI::DrawWithBlurTexture(const ImDrawList* parent_list, const ImDr bool FullscreenUI::UpdateLayoutScale() { #ifndef __ANDROID__ - static constexpr float LAYOUT_RATIO = LAYOUT_SCREEN_WIDTH / LAYOUT_SCREEN_HEIGHT; const ImGuiIO& io = ImGui::GetIO(); @@ -1326,7 +1495,6 @@ bool FullscreenUI::UpdateLayoutScale() return (UIStyle.LayoutScale != old_scale); #else - // On Android, treat a rotated display as always being in landscape mode for FSUI scaling. // Makes achievement popups readable regardless of the device's orientation, and avoids layout changes. const ImGuiIO& io = ImGui::GetIO(); @@ -2823,8 +2991,8 @@ begin: #undef IDX #undef VTX - // Edge case: calling RenderText() with unloaded glyphs triggering texture change. It doesn't happen via ImGui:: calls - // because CalcTextSize() is always used. + // Edge case: calling RenderText() with unloaded glyphs triggering texture change. It doesn't happen via ImGui:: + // calls because CalcTextSize() is always used. if (cmd_count != draw_list->CmdBuffer.Size) //-V547 { IM_ASSERT(draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ElemCount == 0); @@ -2834,8 +3002,8 @@ begin: // IMGUI_DEBUG_LOG("RenderText: cancel and retry to missing glyphs.\n"); // [DEBUG] // draw_list->AddRectFilled(pos, pos + ImVec2(10, 10), IM_COL32(255, 0, 0, 255)); // [DEBUG] goto begin; - // RenderText(draw_list, size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip); // FIXME-OPT: - // Would a 'goto begin' be better for code-gen? return; + // RenderText(draw_list, size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip); // + // FIXME-OPT: Would a 'goto begin' be better for code-gen? return; } // Give back unused vertices (clipped ones, blanks) ~ this is essentially a PrimUnreserve() action. diff --git a/src/core/fullscreenui_widgets.h b/src/core/fullscreenui_widgets.h index 7c871bbc8..615e3291f 100644 --- a/src/core/fullscreenui_widgets.h +++ b/src/core/fullscreenui_widgets.h @@ -257,12 +257,16 @@ bool UpdateLayoutScale(); /// Texture cache. const std::shared_ptr& GetPlaceholderTexture(); -std::shared_ptr LoadTexture(std::string_view path, u32 svg_width = 0, u32 svg_height = 0); +std::shared_ptr LoadTexture(std::string_view path); +std::shared_ptr LoadTexture(std::string_view path, std::string_view name); +std::shared_ptr LoadTexture(std::string_view path, u32 svg_width, u32 svg_height); GPUTexture* FindCachedTexture(std::string_view name); GPUTexture* FindCachedTexture(std::string_view name, u32 svg_width, u32 svg_height); GPUTexture* GetCachedTexture(std::string_view name); +GPUTexture* GetCachedTexture(std::string_view path, std::string_view name); GPUTexture* GetCachedTexture(std::string_view name, u32 svg_width, u32 svg_height); GPUTexture* GetCachedTextureAsync(std::string_view name); +GPUTexture* GetCachedTextureAsync(std::string_view path, std::string_view name); GPUTexture* GetCachedTextureAsync(std::string_view name, u32 svg_width, u32 svg_height); bool InvalidateCachedTexture(std::string_view path); bool TextureNeedsSVGDimensions(std::string_view path);