Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async texture load on Pause screen #18538

Merged
merged 4 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 91 additions & 87 deletions Common/Render/ManagedTexture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -116,7 +136,7 @@ bool TempImage::LoadTextureLevels(const uint8_t *data, size_t size, ImageFileTyp
return numLevels > 0;
}

Draw::Texture *CreateTextureFromTempImage(Draw::DrawContext *draw, const uint8_t *data, size_t dataSize, const TempImage &image, bool generateMips, const char *name) {
Draw::Texture *CreateTextureFromTempImage(Draw::DrawContext *draw, const TempImage &image, bool generateMips, const char *name) {
using namespace Draw;
_assert_(image.levels[0] != nullptr && image.width[0] > 0 && image.height[0] > 0);

Expand Down Expand Up @@ -147,7 +167,7 @@ Draw::Texture *CreateTextureFromFileData(Draw::DrawContext *draw, const uint8_t
if (!image.LoadTextureLevels(data, dataSize, type)) {
return nullptr;
}
Draw::Texture *texture = CreateTextureFromTempImage(draw, data, dataSize, image, generateMips, name);
Draw::Texture *texture = CreateTextureFromTempImage(draw, image, generateMips, name);
image.Free();
return texture;
}
Expand All @@ -165,63 +185,56 @@ Draw::Texture *CreateTextureFromFile(Draw::DrawContext *draw, const char *filena
return texture;
}

bool ManagedTexture::LoadFromFileData(const uint8_t *data, size_t dataSize, ImageFileType type, bool generateMips, const char *name) {
generateMips_ = generateMips;

// 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(data, dataSize, type)) {
return false;
}
texture_ = CreateTextureFromTempImage(draw_, data, dataSize, image, generateMips, name);
image.Free();
return texture_ != nullptr;
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();
}

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;
}
bool retval = LoadFromFileData(buffer, fileSize, type, generateMips, filename.c_str());
if (retval) {
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 retval;
if (texture_)
texture_->Release();
}

std::unique_ptr<ManagedTexture> CreateManagedTextureFromFile(Draw::DrawContext *draw, const char *filename, ImageFileType type, bool generateMips) {
INFO_LOG(SYSTEM, "ManagedTexture::CreateFromFile (%s)", filename);
if (!draw)
return std::unique_ptr<ManagedTexture>();
// 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<ManagedTexture>();
}
return std::unique_ptr<ManagedTexture>(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) {
Expand All @@ -235,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_;
}
63 changes: 49 additions & 14 deletions Common/Render/ManagedTexture.h
Original file line number Diff line number Diff line change
@@ -1,46 +1,81 @@
#pragma once

#include <cstring>
#include <string_view>
#include <memory>

#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 ManagedTexture {
public:
ManagedTexture(Draw::DrawContext *draw) : draw_(draw) {
}
~ManagedTexture() {
if (texture_)
texture_->Release();
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));
}
}
};

bool LoadFromFile(const std::string &filename, ImageFileType type = ImageFileType::DETECT, bool generateMips = false);
bool LoadFromFileData(const uint8_t *data, size_t dataSize, ImageFileType type, bool generateMips, const char *name);
// Managed (will auto-reload from file) and async. For use in UI.
class ManagedTexture {
public:
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(); }

void DeviceLost();
void DeviceRestored(Draw::DrawContext *draw);

bool Failed() const {
return state_ == LoadState::FAILED;
}

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<ManagedTexture> CreateManagedTextureFromFile(Draw::DrawContext *draw, const char *filename, ImageFileType fileType, bool generateMips);
5 changes: 5 additions & 0 deletions Common/Thread/Waitable.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down
Loading