FullscreenUI: Support loading images from URLs with caching

pull/3715/head
Stenzek 6 months ago
parent 2f3e30ea00
commit 10a8f5e195
No known key found for this signature in database

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// 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<Image> LoadTextureImage(std::string_view path, u32 svg_width, u32 svg_height);
static std::optional<Image> LoadTextureImage(std::string_view filename, std::span<const u8> buffer, u32 svg_width,
u32 svg_height);
static std::shared_ptr<GPUTexture> UploadTexture(std::string_view path, const Image& image);
static void QueueTextureUploadFromBuffer(std::string_view filename, const std::span<const u8>& buffer,
std::string&& insert_name, u32 svg_width, u32 svg_height,
bool use_task_for_decode);
static std::shared_ptr<GPUTexture> 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<Image> FullscreenUI::LoadTextureImage(std::string_view path, u32 s
return image;
}
std::optional<Image> FullscreenUI::LoadTextureImage(std::string_view filename, std::span<const u8> buffer,
u32 svg_width, u32 svg_height)
{
std::optional<Image> 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<GPUTexture> FullscreenUI::UploadTexture(std::string_view path, const Image& image)
{
Error error;
@ -688,9 +727,87 @@ std::shared_ptr<GPUTexture> FullscreenUI::UploadTexture(std::string_view path, c
return std::shared_ptr<GPUTexture>(texture.release(), GPUDevice::PooledTextureDeleter());
}
std::shared_ptr<GPUTexture> FullscreenUI::LoadTexture(std::string_view path, u32 width_hint, u32 height_hint)
void FullscreenUI::QueueTextureUploadFromBuffer(std::string_view filename, const std::span<const u8>& buffer,
std::string&& insert_name, u32 svg_width, u32 svg_height,
bool use_task_for_decode)
{
std::optional<Image> 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<u8>(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> 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<GPUTexture> 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<const u8> 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> image = LoadTextureImage(filename, result.value().cspan(), svg_width, svg_height);
if (image.has_value())
{
std::shared_ptr<GPUTexture> 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> image(LoadTextureImage(path, svg_width, svg_height));
if (image.has_value())
{
std::shared_ptr<GPUTexture> ret(UploadTexture(path, image.value()));
@ -701,6 +818,29 @@ std::shared_ptr<GPUTexture> FullscreenUI::LoadTexture(std::string_view path, u32
return s_state.placeholder_texture;
}
std::shared_ptr<GPUTexture> FullscreenUI::LoadTexture(std::string_view name)
{
return LoadTexture(name, {}, 0, 0);
}
std::shared_ptr<GPUTexture> FullscreenUI::LoadTexture(std::string_view path, std::string_view name)
{
return LoadTexture(path, name, 0, 0);
}
std::shared_ptr<GPUTexture> 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<u32>(std::ceil(LayoutScale(static_cast<float>(svg_width))));
svg_height = static_cast<u32>(std::ceil(LayoutScale(static_cast<float>(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<GPUTexture>* 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<GPUTexture>* tex_ptr = s_state.texture_cache.Lookup(name);
if (!tex_ptr)
{
std::shared_ptr<GPUTexture> tex = LoadTexture(name);
std::shared_ptr<GPUTexture> 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<GPUTexture>* 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<GPUTexture>* 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> 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<const u8>& 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> 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<u32>(std::ceil(LayoutScale(static_cast<float>(svg_width))));
svg_height = static_cast<u32>(std::ceil(LayoutScale(static_cast<float>(svg_height))));
const SmallString wh_name = SmallString::from_format("{}#{}x{}", name, svg_width, svg_height);
std::shared_ptr<GPUTexture>* 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> 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.

@ -257,12 +257,16 @@ bool UpdateLayoutScale();
/// Texture cache.
const std::shared_ptr<GPUTexture>& GetPlaceholderTexture();
std::shared_ptr<GPUTexture> LoadTexture(std::string_view path, u32 svg_width = 0, u32 svg_height = 0);
std::shared_ptr<GPUTexture> LoadTexture(std::string_view path);
std::shared_ptr<GPUTexture> LoadTexture(std::string_view path, std::string_view name);
std::shared_ptr<GPUTexture> 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);

Loading…
Cancel
Save