From f8927df9a644fb6837f96a45280b616fc5a05877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 13 Dec 2023 11:45:48 +0100 Subject: [PATCH] Load savestate screenshots in the background --- Common/Render/ManagedTexture.cpp | 171 ++++++++++++++++--------------- Common/Render/ManagedTexture.h | 56 ++++++++-- Common/Thread/Waitable.h | 5 + Common/UI/AsyncImageFileView.cpp | 2 +- UI/MiscScreens.cpp | 2 +- UI/Store.cpp | 2 +- 6 files changed, 143 insertions(+), 95 deletions(-) diff --git a/Common/Render/ManagedTexture.cpp b/Common/Render/ManagedTexture.cpp index 1a894acc16ae..0aeef5ba513d 100644 --- a/Common/Render/ManagedTexture.cpp +++ b/Common/Render/ManagedTexture.cpp @@ -16,31 +16,51 @@ #include "Common/Log.h" #include "Common/TimeUtil.h" #include "Common/Render/ManagedTexture.h" +#include "Common/Thread/ThreadManager.h" +#include "Common/Thread/Waitable.h" + +// TODO: It really feels like we should be able to simplify this. +class TextureLoadTask : public Task { +public: + TextureLoadTask(std::string_view filename, ImageFileType type, bool generateMips, TempImage *tempImage, ManagedTexture::LoadState *state, LimitedWaitable *waitable) + : filename_(filename), type_(type), generateMips_(generateMips), tempImage_(tempImage), state_(state), waitable_(waitable) {} + + TaskType Type() const override { return TaskType::IO_BLOCKING; } + TaskPriority Priority() const override { return TaskPriority::NORMAL; } + + void Run() override { + size_t fileSize; + uint8_t *buffer = g_VFS.ReadFile(filename_.c_str(), &fileSize); + if (!buffer) { + filename_.clear(); + ERROR_LOG(IO, "Failed to read file '%s'", filename_.c_str()); + *state_ = ManagedTexture::LoadState::FAILED; + return; + } -// For UI images loaded from disk, loaded into RAM, generally staged for upload. -// The reason for the separation is so that the image can be loaded and decompressed on a thread, -// and then only uploaded to the GPU on the main thread. -struct TempImage { - ~TempImage() { - _dbg_assert_(levels[0] == nullptr); - } - Draw::DataFormat fmt = Draw::DataFormat::UNDEFINED; - ImageFileType type = TYPE_UNKNOWN; - uint8_t *levels[16]{}; // only free the first pointer, they all point to the same buffer. - int zimFlags = 0; - int width[16]{}; - int height[16]{}; - int numLevels = 0; - - bool LoadTextureLevels(const uint8_t *data, size_t size, ImageFileType typeSuggestion = DETECT); - void Free() { - if (levels[0]) { - free(levels[0]); - memset(levels, 0, sizeof(levels)); + if (!tempImage_->LoadTextureLevels(buffer, fileSize, type_)) { + *state_ = ManagedTexture::LoadState::FAILED; + return; } - } + delete[] buffer; + *state_ = ManagedTexture::LoadState::SUCCESS; + waitable_->Notify(); + } + +private: + LimitedWaitable *waitable_; + std::string filename_; + TempImage *tempImage_; + ImageFileType type_; + bool generateMips_; + ManagedTexture::LoadState *state_; }; +TempImage::~TempImage() { + // Make sure you haven't forgotten to call Free. + _dbg_assert_(levels[0] == nullptr); +} + static Draw::DataFormat ZimToT3DFormat(int zim) { switch (zim) { case ZIM_RGBA8888: return Draw::DataFormat::R8G8B8A8_UNORM; @@ -50,24 +70,24 @@ static Draw::DataFormat ZimToT3DFormat(int zim) { static ImageFileType DetectImageFileType(const uint8_t *data, size_t size) { if (size < 4) { - return TYPE_UNKNOWN; + return ImageFileType::UNKNOWN; } if (!memcmp(data, "ZIMG", 4)) { - return ZIM; + return ImageFileType::ZIM; } else if (!memcmp(data, "\x89\x50\x4E\x47", 4)) { - return PNG; + return ImageFileType::PNG; } else if (!memcmp(data, "\xff\xd8\xff\xe0", 4) || !memcmp(data, "\xff\xd8\xff\xe1", 4)) { - return JPEG; + return ImageFileType::JPEG; } else { - return TYPE_UNKNOWN; + return ImageFileType::UNKNOWN; } } bool TempImage::LoadTextureLevels(const uint8_t *data, size_t size, ImageFileType typeSuggestion) { - if (typeSuggestion == DETECT) { + if (typeSuggestion == ImageFileType::DETECT) { typeSuggestion = DetectImageFileType(data, size); } - if (typeSuggestion == TYPE_UNKNOWN) { + if (typeSuggestion == ImageFileType::UNKNOWN) { ERROR_LOG(G3D, "File (size: %d) has unknown format", (int)size); return false; } @@ -77,12 +97,12 @@ bool TempImage::LoadTextureLevels(const uint8_t *data, size_t size, ImageFileTyp zimFlags = 0; switch (typeSuggestion) { - case ZIM: + case ImageFileType::ZIM: numLevels = LoadZIMPtr((const uint8_t *)data, size, width, height, &zimFlags, levels); fmt = ZimToT3DFormat(zimFlags & ZIM_FORMAT_MASK); break; - case PNG: + case ImageFileType::PNG: if (1 == pngLoadPtr((const unsigned char *)data, size, &width[0], &height[0], &levels[0])) { numLevels = 1; fmt = Draw::DataFormat::R8G8B8A8_UNORM; @@ -96,7 +116,7 @@ bool TempImage::LoadTextureLevels(const uint8_t *data, size_t size, ImageFileTyp } break; - case JPEG: + case ImageFileType::JPEG: { int actual_components = 0; unsigned char *jpegBuf = jpgd::decompress_jpeg_image_from_memory(data, (int)size, &width[0], &height[0], &actual_components, 4); @@ -165,58 +185,56 @@ Draw::Texture *CreateTextureFromFile(Draw::DrawContext *draw, const char *filena return texture; } -bool ManagedTexture::LoadFromFile(const std::string &filename, ImageFileType type, bool generateMips) { - INFO_LOG(SYSTEM, "ManagedTexture::LoadFromFile (%s)", filename.c_str()); - generateMips_ = generateMips; - size_t fileSize; - uint8_t *buffer = g_VFS.ReadFile(filename.c_str(), &fileSize); - if (!buffer) { - filename_.clear(); - ERROR_LOG(IO, "Failed to read file '%s'", filename.c_str()); - return false; - } - - // Free the old texture, if any. +Draw::Texture *ManagedTexture::GetTexture() { if (texture_) { - texture_->Release(); - texture_ = nullptr; + return texture_; + } else if (state_ == LoadState::SUCCESS) { + if (taskWaitable_) { + taskWaitable_->WaitAndRelease(); + taskWaitable_ = nullptr; + } + // Image load is done, texture creation is not. + texture_ = CreateTextureFromTempImage(draw_, pendingImage_, generateMips_, filename_.c_str()); + pendingImage_.Free(); } + return texture_; +} - TempImage image; - if (!image.LoadTextureLevels(buffer, fileSize, type)) { - return false; - } - texture_ = CreateTextureFromTempImage(draw_, image, generateMips, filename.c_str()); - image.Free(); +ManagedTexture::ManagedTexture(Draw::DrawContext *draw, std::string_view filename, ImageFileType type, bool generateMips) + : draw_(draw), filename_(filename), type_(type), generateMips_(generateMips) +{ + INFO_LOG(SYSTEM, "ManagedTexture::ManagedTexture (%s)", filename_.c_str()); + StartLoadTask(); +} - if (texture_) { - filename_ = filename; - } else { - filename_.clear(); - ERROR_LOG(IO, "Failed to load texture '%s'", filename.c_str()); +ManagedTexture::~ManagedTexture() { + // Stop any pending loads. + if (taskWaitable_) { + taskWaitable_->WaitAndRelease(); + pendingImage_.Free(); } - delete[] buffer; - return texture_ != 0; + if (texture_) + texture_->Release(); } -std::unique_ptr CreateManagedTextureFromFile(Draw::DrawContext *draw, const char *filename, ImageFileType type, bool generateMips) { - INFO_LOG(SYSTEM, "ManagedTexture::CreateFromFile (%s)", filename); - if (!draw) - return std::unique_ptr(); - // TODO: Load the texture on a background thread. - ManagedTexture *mtex = new ManagedTexture(draw); - if (!mtex->LoadFromFile(filename, type, generateMips)) { - delete mtex; - return std::unique_ptr(); - } - return std::unique_ptr(mtex); +void ManagedTexture::StartLoadTask() { + _dbg_assert_(!taskWaitable_); + taskWaitable_ = new LimitedWaitable(); + g_threadManager.EnqueueTask(new TextureLoadTask(filename_, type_, generateMips_, &pendingImage_, &state_, taskWaitable_)); } void ManagedTexture::DeviceLost() { INFO_LOG(G3D, "ManagedTexture::DeviceLost(%s)", filename_.c_str()); + if (taskWaitable_) { + taskWaitable_->WaitAndRelease(); + pendingImage_.Free(); + } if (texture_) texture_->Release(); texture_ = nullptr; + if (state_ == LoadState::SUCCESS) { + state_ = LoadState::PENDING; + } } void ManagedTexture::DeviceRestored(Draw::DrawContext *draw) { @@ -230,17 +248,8 @@ void ManagedTexture::DeviceRestored(Draw::DrawContext *draw) { return; } - // Vulkan: Can't load textures before the first frame has started. - // Should probably try to lift that restriction again someday.. - loadPending_ = true; -} - -Draw::Texture *ManagedTexture::GetTexture() { - if (loadPending_) { - if (!LoadFromFile(filename_, ImageFileType::DETECT, generateMips_)) { - ERROR_LOG(IO, "ManagedTexture failed: '%s'", filename_.c_str()); - } - loadPending_ = false; + if (state_ == LoadState::PENDING) { + // Kick off a new load task. + StartLoadTask(); } - return texture_; } diff --git a/Common/Render/ManagedTexture.h b/Common/Render/ManagedTexture.h index 8db54592101a..ce6554e0981b 100644 --- a/Common/Render/ManagedTexture.h +++ b/Common/Render/ManagedTexture.h @@ -1,27 +1,51 @@ #pragma once +#include +#include #include #include "Common/GPU/thin3d.h" #include "Common/UI/View.h" #include "Common/File/Path.h" -enum ImageFileType { +enum class ImageFileType { PNG, JPEG, ZIM, DETECT, - TYPE_UNKNOWN, + UNKNOWN, }; +class TextureLoadTask; +class LimitedWaitable; + +// For UI images loaded from disk, loaded into RAM, generally staged for upload. +// The reason for the separation is so that the image can be loaded and decompressed on a thread, +// and then only uploaded to the GPU on the main thread. +struct TempImage { + ~TempImage(); + Draw::DataFormat fmt = Draw::DataFormat::UNDEFINED; + ImageFileType type = ImageFileType::UNKNOWN; + uint8_t *levels[16]{}; // only free the first pointer, they all point to the same buffer. + int zimFlags = 0; + int width[16]{}; + int height[16]{}; + int numLevels = 0; + + bool LoadTextureLevels(const uint8_t *data, size_t size, ImageFileType typeSuggestion = ImageFileType::DETECT); + void Free() { + if (levels[0]) { + free(levels[0]); + memset(levels, 0, sizeof(levels)); + } + } +}; + +// Managed (will auto-reload from file) and async. For use in UI. class ManagedTexture { public: - ManagedTexture(Draw::DrawContext *draw) : draw_(draw) {} - ~ManagedTexture() { - if (texture_) - texture_->Release(); - } - bool LoadFromFile(const std::string &filename, ImageFileType type = ImageFileType::DETECT, bool generateMips = false); + ManagedTexture(Draw::DrawContext *draw, std::string_view filename, ImageFileType type = ImageFileType::DETECT, bool generateMips = false); + ~ManagedTexture(); Draw::Texture *GetTexture(); // For immediate use, don't store. int Width() const { return texture_->Width(); } int Height() const { return texture_->Height(); } @@ -29,15 +53,25 @@ class ManagedTexture { void DeviceLost(); void DeviceRestored(Draw::DrawContext *draw); + enum class LoadState { + PENDING, + FAILED, + SUCCESS, + }; + private: + void StartLoadTask(); + Draw::Texture *texture_ = nullptr; Draw::DrawContext *draw_; std::string filename_; // Textures that are loaded from files can reload themselves automatically. bool generateMips_ = false; - bool loadPending_ = false; + ImageFileType type_ = ImageFileType::DETECT; + TextureLoadTask *loadTask_ = nullptr; + LimitedWaitable *taskWaitable_ = nullptr; + TempImage pendingImage_; + LoadState state_ = LoadState::PENDING; }; Draw::Texture *CreateTextureFromFileData(Draw::DrawContext *draw, const uint8_t *data, size_t dataSize, ImageFileType type, bool generateMips, const char *name); Draw::Texture *CreateTextureFromFile(Draw::DrawContext *draw, const char *filename, ImageFileType type, bool generateMips); - -std::unique_ptr CreateManagedTextureFromFile(Draw::DrawContext *draw, const char *filename, ImageFileType fileType, bool generateMips); diff --git a/Common/Thread/Waitable.h b/Common/Thread/Waitable.h index dcceefd046c8..de33ed2049f1 100644 --- a/Common/Thread/Waitable.h +++ b/Common/Thread/Waitable.h @@ -43,6 +43,11 @@ class LimitedWaitable : public Waitable { cond_.notify_all(); } + // For simple polling. + bool Ready() const { + return triggered_; + } + private: std::condition_variable cond_; std::mutex mutex_; diff --git a/Common/UI/AsyncImageFileView.cpp b/Common/UI/AsyncImageFileView.cpp index 9948a9986fe3..117e35448e90 100644 --- a/Common/UI/AsyncImageFileView.cpp +++ b/Common/UI/AsyncImageFileView.cpp @@ -79,7 +79,7 @@ void AsyncImageFileView::DeviceRestored(Draw::DrawContext *draw) { void AsyncImageFileView::Draw(UIContext &dc) { using namespace Draw; if (!texture_ && !textureFailed_ && !filename_.empty()) { - texture_ = CreateManagedTextureFromFile(dc.GetDrawContext(), filename_.c_str(), DETECT, true); + texture_ = std::unique_ptr(new ManagedTexture(dc.GetDrawContext(), filename_.c_str(), ImageFileType::DETECT, true)); if (!texture_.get()) textureFailed_ = true; } diff --git a/UI/MiscScreens.cpp b/UI/MiscScreens.cpp index da9b5b0bd6c7..d0d212760a74 100644 --- a/UI/MiscScreens.cpp +++ b/UI/MiscScreens.cpp @@ -296,7 +296,7 @@ void UIBackgroundInit(UIContext &dc) { const Path bgJpg = GetSysDirectory(DIRECTORY_SYSTEM) / "background.jpg"; if (File::Exists(bgPng) || File::Exists(bgJpg)) { const Path &bgFile = File::Exists(bgPng) ? bgPng : bgJpg; - bgTexture = CreateTextureFromFile(dc.GetDrawContext(), bgFile.c_str(), DETECT, true); + bgTexture = CreateTextureFromFile(dc.GetDrawContext(), bgFile.c_str(), ImageFileType::DETECT, true); } } diff --git a/UI/Store.cpp b/UI/Store.cpp index 8f31b2711952..83618d55ef8d 100644 --- a/UI/Store.cpp +++ b/UI/Store.cpp @@ -183,7 +183,7 @@ void HttpImageFileView::Draw(UIContext &dc) { } if (!textureData_.empty()) { - texture_ = CreateTextureFromFileData(dc.GetDrawContext(), (const uint8_t *)(textureData_.data()), (int)textureData_.size(), DETECT, false, "store_icon"); + texture_ = CreateTextureFromFileData(dc.GetDrawContext(), (const uint8_t *)(textureData_.data()), (int)textureData_.size(), ImageFileType::DETECT, false, "store_icon"); if (!texture_) textureFailed_ = true; textureData_.clear();