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

Replacement: Do all I/O on threaded tasks #17091

Merged
merged 7 commits into from
Mar 11, 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
191 changes: 86 additions & 105 deletions GPU/Common/ReplacedTexture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,32 +90,49 @@ bool ReplacedTexture::IsReady(double budget) {

_assert_(!threadWaitable_);
threadWaitable_ = new LimitedWaitable();
SetState(ReplacementState::PENDING);
g_threadManager.EnqueueTask(new ReplacedTextureTask(vfs_, *this, threadWaitable_));
if (threadWaitable_->WaitFor(budget)) {
// If we successfully wait here, we're done. The thread will set state accordingly.
_assert_(State() == ReplacementState::ACTIVE || State() == ReplacementState::NOT_FOUND || State() == ReplacementState::CANCEL_INIT);
return true;
}
SetState(ReplacementState::PENDING);
// Still pending on thread.
return false;
}

void ReplacedTexture::FinishPopulate(const ReplacementDesc &desc) {
logId_ = desc.logId;
levelData_ = desc.cache;
void ReplacedTexture::FinishPopulate(ReplacementDesc *desc) {
logId_ = desc->logId;
levelData_ = desc->cache;
desc_ = desc;
SetState(ReplacementState::POPULATED);

// TODO: What used to be here is now done on the thread task.
}

// TODO: The rest can be done on the thread.
void ReplacedTexture::Prepare(VFSBackend *vfs) {
this->vfs_ = vfs;

std::unique_lock<std::mutex> lock(mutex_);

_assert_msg_(levelData_ != nullptr, "Level cache not set");

// We must lock around access to levelData_ in case two textures try to load it at once.
std::lock_guard<std::mutex> guard(levelData_->lock);

for (int i = 0; i < std::min(MAX_REPLACEMENT_MIP_LEVELS, (int)desc_->filenames.size()); ++i) {
if (State() == ReplacementState::CANCEL_INIT) {
break;
}

for (int i = 0; i < std::min(MAX_REPLACEMENT_MIP_LEVELS, (int)desc.filenames.size()); ++i) {
if (desc.filenames[i].empty()) {
if (desc_->filenames[i].empty()) {
// Out of valid mip levels. Bail out.
break;
}

const Path filename = desc.basePath / desc.filenames[i];
const Path filename = desc_->basePath / desc_->filenames[i];

VFSFileReference *fileRef = vfs_->GetFile(desc.filenames[i].c_str());
VFSFileReference *fileRef = vfs_->GetFile(desc_->filenames[i].c_str());
if (!fileRef) {
// If the file doesn't exist, let's just bail immediately here.
break;
Expand All @@ -130,58 +147,43 @@ void ReplacedTexture::FinishPopulate(const ReplacementDesc &desc) {
fmt = Draw::DataFormat::R8G8B8A8_UNORM;
}

bool good;

level.fileRef = fileRef;
good = PopulateLevel(level, false);

// We pad files that have been hashrange'd so they are the same texture size.
level.w = (level.w * desc.w) / desc.newW;
level.h = (level.h * desc.h) / desc.newH;

if (good && i != 0) {
// Check that the mipmap size is correct. Can't load mips of the wrong size.
if (level.w != (levels_[0].w >> i) || level.h != (levels_[0].h >> i)) {
WARN_LOG(G3D, "Replacement mipmap invalid: size=%dx%d, expected=%dx%d (level %d, '%s')", level.w, level.h, levels_[0].w >> i, levels_[0].h >> i, i, filename.c_str());
good = false;
}
}

if (good)
if (LoadLevelData(level, i)) {
levels_.push_back(level);
// Otherwise, we're done loading mips (bad PNG or bad size, either way.)
else
} else {
// Otherwise, we're done loading mips (bad PNG or bad size, either way.)
break;
}
}

delete desc_;
desc_ = nullptr;

if (levels_.empty()) {
// Bad.
SetState(ReplacementState::NOT_FOUND);
levelData_ = nullptr;
return;
}

// Populate the data pointer.
SetState(ReplacementState::POPULATED);
SetState(ReplacementState::ACTIVE);

if (threadWaitable_)
threadWaitable_->Notify();
}

bool ReplacedTexture::PopulateLevel(ReplacedTextureLevel &level, bool ignoreError) {
bool ReplacedTexture::LoadLevelData(ReplacedTextureLevel &level, int mipLevel) {
bool good = false;

if (!level.fileRef) {
if (!ignoreError)
ERROR_LOG(G3D, "Error opening replacement texture file '%s' in textures.zip", level.file.c_str());
return false;
}

size_t fileSize;
VFSOpenFile *file = vfs_->OpenFileForRead(level.fileRef, &fileSize);
if (!file) {
VFSOpenFile *openFile = vfs_->OpenFileForRead(level.fileRef, &fileSize);
if (!openFile) {
return false;
}

std::string magic;
auto imageType = Identify(vfs_, file, &magic);
ReplacedImageType imageType = Identify(vfs_, openFile, &magic);

if (imageType == ReplacedImageType::ZIM) {
uint32_t ignore = 0;
Expand All @@ -191,13 +193,13 @@ bool ReplacedTexture::PopulateLevel(ReplacedTextureLevel &level, bool ignoreErro
uint32_t h;
uint32_t flags;
} header;
good = vfs_->Read(file, &header, sizeof(header)) == sizeof(header);
good = vfs_->Read(openFile, &header, sizeof(header)) == sizeof(header);
level.w = header.w;
level.h = header.h;
good = (header.flags & ZIM_FORMAT_MASK) == ZIM_RGBA8888;
} else if (imageType == ReplacedImageType::PNG) {
PNGHeaderPeek headerPeek;
good = vfs_->Read(file, &headerPeek, sizeof(headerPeek)) == sizeof(headerPeek);
good = vfs_->Read(openFile, &headerPeek, sizeof(headerPeek)) == sizeof(headerPeek);
if (good && headerPeek.IsValidPNGHeader()) {
level.w = headerPeek.Width();
level.h = headerPeek.Height();
Expand All @@ -209,101 +211,80 @@ bool ReplacedTexture::PopulateLevel(ReplacedTextureLevel &level, bool ignoreErro
} else {
ERROR_LOG(G3D, "Could not load texture replacement info: %s - unsupported format %s", level.file.ToVisualString().c_str(), magic.c_str());
}
vfs_->CloseFile(file);

return good;
}

void ReplacedTexture::Prepare(VFSBackend *vfs) {
std::unique_lock<std::mutex> lock(mutex_);
this->vfs_ = vfs;
// Is this really the right place to do it?
level.w = (level.w * desc_->w) / desc_->newW;
level.h = (level.h * desc_->h) / desc_->newH;

if (State() == ReplacementState::CANCEL_INIT) {
return;
if (good && mipLevel != 0) {
// Check that the mipmap size is correct. Can't load mips of the wrong size.
if (level.w != (levels_[0].w >> mipLevel) || level.h != (levels_[0].h >> mipLevel)) {
WARN_LOG(G3D, "Replacement mipmap invalid: size=%dx%d, expected=%dx%d (level %d)",
level.w, level.h, levels_[0].w >> mipLevel, levels_[0].h >> mipLevel, mipLevel);
good = false;
}
}

for (int i = 0; i < (int)levels_.size(); ++i) {
if (State() == ReplacementState::CANCEL_INIT)
break;
PrepareData(i);
if (!good) {
return false;
}

if (threadWaitable_)
threadWaitable_->Notify();
}

void ReplacedTexture::PrepareData(int level) {
_assert_msg_((size_t)level < levels_.size(), "Invalid miplevel");
_assert_msg_(levelData_ != nullptr, "Level cache not set");

// We must lock around access to levelData_ in case two textures try to load it at once.
std::lock_guard<std::mutex> guard(levelData_->lock);

const ReplacedTextureLevel &info = levels_[level];

if (levelData_->data.size() <= level) {
levelData_->data.resize(level + 1);
if (levelData_->data.size() <= mipLevel) {
levelData_->data.resize(mipLevel + 1);
}

std::vector<uint8_t> &out = levelData_->data[level];
std::vector<uint8_t> &out = levelData_->data[mipLevel];

// Already populated from cache.
if (!out.empty()) {
SetState(ReplacementState::ACTIVE);
return;
return true;
}

ReplacedImageType imageType;

size_t fileSize;
VFSOpenFile *openFile = vfs_->OpenFileForRead(info.fileRef, &fileSize);

std::string magic;
imageType = Identify(vfs_, openFile, &magic);

auto cleanup = [&] {
vfs_->CloseFile(openFile);
};

vfs_->Rewind(openFile);

if (imageType == ReplacedImageType::ZIM) {
std::unique_ptr<uint8_t[]> zim(new uint8_t[fileSize]);
if (!zim) {
ERROR_LOG(G3D, "Failed to allocate memory for texture replacement");
SetState(ReplacementState::NOT_FOUND);
cleanup();
return;
return false;
}

if (vfs_->Read(openFile, &zim[0], fileSize) != fileSize) {
ERROR_LOG(G3D, "Could not load texture replacement: %s - failed to read ZIM", info.file.c_str());
ERROR_LOG(G3D, "Could not load texture replacement: %s - failed to read ZIM", level.file.c_str());
SetState(ReplacementState::NOT_FOUND);
cleanup();
return;
return false;
}

int w, h, f;
uint8_t *image;
if (LoadZIMPtr(&zim[0], fileSize, &w, &h, &f, &image)) {
if (w > info.w || h > info.h) {
ERROR_LOG(G3D, "Texture replacement changed since header read: %s", info.file.c_str());
if (w > level.w || h > level.h) {
ERROR_LOG(G3D, "Texture replacement changed since header read: %s", level.file.c_str());
SetState(ReplacementState::NOT_FOUND);
cleanup();
return;
return false;
}

out.resize(info.w * info.h * 4);
if (w == info.w) {
memcpy(&out[0], image, info.w * 4 * info.h);
out.resize(level.w * level.h * 4);
if (w == level.w) {
memcpy(&out[0], image, level.w * 4 * level.h);
} else {
for (int y = 0; y < h; ++y) {
memcpy(&out[info.w * 4 * y], image + w * 4 * y, w * 4);
memcpy(&out[level.w * 4 * y], image + w * 4 * y, w * 4);
}
}
free(image);
}

CheckAlphaResult res = CheckAlpha32Rect((u32 *)&out[0], info.w, w, h, 0xFF000000);
if (res == CHECKALPHA_ANY || level == 0) {
CheckAlphaResult res = CheckAlpha32Rect((u32 *)&out[0], level.w, w, h, 0xFF000000);
if (res == CHECKALPHA_ANY || mipLevel == 0) {
alphaStatus_ = ReplacedTextureAlpha(res);
}
} else if (imageType == ReplacedImageType::PNG) {
Expand All @@ -314,49 +295,49 @@ void ReplacedTexture::PrepareData(int level) {
pngdata.resize(fileSize);
pngdata.resize(vfs_->Read(openFile, &pngdata[0], fileSize));
if (!png_image_begin_read_from_memory(&png, &pngdata[0], pngdata.size())) {
ERROR_LOG(G3D, "Could not load texture replacement info: %s - %s (zip)", info.file.c_str(), png.message);
ERROR_LOG(G3D, "Could not load texture replacement info: %s - %s (zip)", level.file.c_str(), png.message);
SetState(ReplacementState::NOT_FOUND);
cleanup();
return;
return false;
}
if (png.width > (uint32_t)info.w || png.height > (uint32_t)info.h) {
ERROR_LOG(G3D, "Texture replacement changed since header read: %s", info.file.c_str());
if (png.width > (uint32_t)level.w || png.height > (uint32_t)level.h) {
ERROR_LOG(G3D, "Texture replacement changed since header read: %s", level.file.c_str());
SetState(ReplacementState::NOT_FOUND);
cleanup();
return;
return false;
}

bool checkedAlpha = false;
if ((png.format & PNG_FORMAT_FLAG_ALPHA) == 0) {
// Well, we know for sure it doesn't have alpha.
if (level == 0) {
if (mipLevel == 0) {
alphaStatus_ = ReplacedTextureAlpha::FULL;
}
checkedAlpha = true;
}
png.format = PNG_FORMAT_RGBA;

out.resize(info.w * info.h * 4);
if (!png_image_finish_read(&png, nullptr, &out[0], info.w * 4, nullptr)) {
ERROR_LOG(G3D, "Could not load texture replacement: %s - %s", info.file.c_str(), png.message);
out.resize(level.w * level.h * 4);
if (!png_image_finish_read(&png, nullptr, &out[0], level.w * 4, nullptr)) {
ERROR_LOG(G3D, "Could not load texture replacement: %s - %s", level.file.c_str(), png.message);
SetState(ReplacementState::NOT_FOUND);
cleanup();
out.resize(0);
return;
return false;
}
png_image_free(&png);

if (!checkedAlpha) {
// This will only check the hashed bits.
CheckAlphaResult res = CheckAlpha32Rect((u32 *)&out[0], info.w, png.width, png.height, 0xFF000000);
if (res == CHECKALPHA_ANY || level == 0) {
CheckAlphaResult res = CheckAlpha32Rect((u32 *)&out[0], level.w, png.width, png.height, 0xFF000000);
if (res == CHECKALPHA_ANY || mipLevel == 0) {
alphaStatus_ = ReplacedTextureAlpha(res);
}
}
}

SetState(ReplacementState::ACTIVE);
cleanup();
return true;
}

void ReplacedTexture::PurgeIfOlder(double t) {
Expand Down
9 changes: 4 additions & 5 deletions GPU/Common/ReplacedTexture.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,14 @@ struct ReplacedTexture {
bool IsReady(double budget);
bool CopyLevelTo(int level, void *out, int rowPitch);

void FinishPopulate(const ReplacementDesc &desc);
void FinishPopulate(ReplacementDesc *desc);
std::string logId_;

private:
void Prepare(VFSBackend *vfs);
void PrepareData(int level);
bool LoadLevelData(ReplacedTextureLevel &info, int level);
void PurgeIfOlder(double t);

bool PopulateLevel(ReplacedTextureLevel & level, bool ignoreError);

std::vector<ReplacedTextureLevel> levels_;
ReplacedLevelsCache *levelData_ = nullptr;

Expand All @@ -148,9 +146,10 @@ struct ReplacedTexture {
std::mutex mutex_;
Draw::DataFormat fmt = Draw::DataFormat::UNDEFINED; // NOTE: Right now, the only supported format is Draw::DataFormat::R8G8B8A8_UNORM.

ReplacementState state_ = ReplacementState::UNINITIALIZED;
std::atomic<ReplacementState> state_ = ReplacementState::UNINITIALIZED;

VFSBackend *vfs_ = nullptr;
ReplacementDesc *desc_ = nullptr;

friend class TextureReplacer;
friend class ReplacedTextureTask;
Expand Down
3 changes: 2 additions & 1 deletion GPU/Common/TextureCacheCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2829,8 +2829,9 @@ bool TextureCacheCommon::PrepareBuildTexture(BuildTexturePlan &plan, TexCacheEnt
if (plan.replaceValid) {
// We're replacing, so we won't scale.
plan.scaleFactor = 1;
// We're ignoring how many levels were specified - instead we just load all available from the replacer.
plan.levelsToLoad = plan.replaced->NumLevels();
plan.levelsToCreate = std::min(plan.levelsToLoad, plan.levelsToCreate);
plan.levelsToCreate = plan.levelsToLoad; // Or more, if we wanted to generate.
plan.badMipSizes = false;
// But, we still need to create the texture at a larger size.
plan.replaced->GetSize(0, &plan.createW, &plan.createH);
Expand Down
Loading