Skip to content

Commit

Permalink
Merge pull request #18538 from hrydgard/async-texture-load
Browse files Browse the repository at this point in the history
Async texture load on Pause screen
  • Loading branch information
hrydgard committed Dec 13, 2023
2 parents 32f5f08 + 1ac780a commit da318e0
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 113 deletions.
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

0 comments on commit da318e0

Please sign in to comment.