custom_tex_manager: Implement hot-reloading
This commit is contained in:
@ -534,7 +534,6 @@ u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry,
|
||||
}
|
||||
|
||||
void GetAllFilesFromNestedEntries(FSTEntry& directory, std::vector<FSTEntry>& output) {
|
||||
std::vector<FSTEntry> files;
|
||||
for (auto& entry : directory.children) {
|
||||
if (entry.isDirectory) {
|
||||
GetAllFilesFromNestedEntries(entry, output);
|
||||
|
@ -19,8 +19,9 @@ static FileAction Win32ActionToFileAction(DWORD action) {
|
||||
case FILE_ACTION_MODIFIED:
|
||||
return FileAction::Modified;
|
||||
case FILE_ACTION_RENAMED_OLD_NAME:
|
||||
return FileAction::RenamedOldName;
|
||||
case FILE_ACTION_RENAMED_NEW_NAME:
|
||||
return FileAction::Renamed;
|
||||
return FileAction::RenamedNewName;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown action {}", action);
|
||||
return FileAction::Invalid;
|
||||
@ -28,11 +29,11 @@ static FileAction Win32ActionToFileAction(DWORD action) {
|
||||
}
|
||||
|
||||
struct FileWatcher::Impl {
|
||||
explicit Impl(const std::wstring& path, FileWatcher::Callback&& callback_)
|
||||
explicit Impl(const std::string& path, FileWatcher::Callback&& callback_)
|
||||
: callback{callback_} {
|
||||
// Create file handle for the directory we are watching.
|
||||
dir_handle =
|
||||
CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | GENERIC_READ,
|
||||
CreateFile(path.c_str(), FILE_LIST_DIRECTORY | GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL);
|
||||
ASSERT_MSG(dir_handle != INVALID_HANDLE_VALUE, "Unable to create watch file");
|
||||
@ -70,6 +71,8 @@ struct FileWatcher::Impl {
|
||||
while (is_running) {
|
||||
bool result =
|
||||
ReadDirectoryChangesW(dir_handle, buffer.data(), buffer.size(), TRUE,
|
||||
FILE_NOTIFY_CHANGE_FILE_NAME |
|
||||
FILE_NOTIFY_CHANGE_DIR_NAME |
|
||||
FILE_NOTIFY_CHANGE_LAST_WRITE, NULL, &overlapped, NULL);
|
||||
ASSERT_MSG(result, "Unable to read directory changes: {}", GetLastErrorMsg());
|
||||
|
||||
@ -101,25 +104,28 @@ struct FileWatcher::Impl {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t next_entry_offset{};
|
||||
FILE_NOTIFY_INFORMATION fni;
|
||||
do {
|
||||
u32 next_entry_offset{};
|
||||
while (true) {
|
||||
// Retrieve file notify information.
|
||||
std::memcpy(&fni, buffer.data() + next_entry_offset, sizeof(fni));
|
||||
next_entry_offset += fni.NextEntryOffset;
|
||||
auto fni = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer.data() + next_entry_offset);
|
||||
|
||||
// Call the callback function informing about the change.
|
||||
if (fni.Action != 0) {
|
||||
const std::wstring file_name{fni.FileName, fni.FileNameLength};
|
||||
const FileAction action = Win32ActionToFileAction(fni.Action);
|
||||
if (fni->Action != 0) {
|
||||
std::string file_name(fni->FileNameLength / sizeof(WCHAR), ' ');
|
||||
WideCharToMultiByte(CP_UTF8, 0, fni->FileName,
|
||||
fni->FileNameLength / sizeof(WCHAR), file_name.data(), file_name.size(), NULL, NULL);
|
||||
const FileAction action = Win32ActionToFileAction(fni->Action);
|
||||
callback(file_name, action);
|
||||
}
|
||||
|
||||
// If this was the last action, break.
|
||||
if (fni.NextEntryOffset == 0) {
|
||||
if (fni->NextEntryOffset == 0) {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
|
||||
// Move to next fni structure.
|
||||
next_entry_offset += fni->NextEntryOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
@ -134,7 +140,9 @@ private:
|
||||
std::thread watch_thread;
|
||||
};
|
||||
|
||||
FileWatcher::FileWatcher(const std::wstring& log_dir, Callback&& callback)
|
||||
FileWatcher::FileWatcher(const std::string& log_dir, Callback&& callback)
|
||||
: impl{std::make_unique<Impl>(log_dir, std::move(callback))} {}
|
||||
|
||||
FileWatcher::~FileWatcher() = default;
|
||||
|
||||
} // namespace Common
|
||||
|
@ -13,15 +13,16 @@ enum class FileAction : u8 {
|
||||
Added,
|
||||
Removed,
|
||||
Modified,
|
||||
Renamed,
|
||||
RenamedOldName,
|
||||
RenamedNewName,
|
||||
Invalid = std::numeric_limits<u8>::max(),
|
||||
};
|
||||
|
||||
class FileWatcher {
|
||||
using Callback = std::function<void(const std::wstring&, FileAction)>;
|
||||
using Callback = std::function<void(const std::string&, FileAction)>;
|
||||
|
||||
public:
|
||||
explicit FileWatcher(const std::wstring& log_dir, Callback&& callback);
|
||||
explicit FileWatcher(const std::string& log_dir, Callback&& callback);
|
||||
~FileWatcher();
|
||||
|
||||
private:
|
||||
|
@ -1,9 +1,10 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma clang optimize off
|
||||
#include <json.hpp>
|
||||
#include "common/file_util.h"
|
||||
#include "common/file_watcher.h"
|
||||
#include "common/memory_detect.h"
|
||||
#include "common/microprofile.h"
|
||||
#include "common/settings.h"
|
||||
@ -16,6 +17,7 @@
|
||||
#include "video_core/custom_textures/custom_tex_manager.h"
|
||||
#include "video_core/rasterizer_cache/surface_params.h"
|
||||
#include "video_core/rasterizer_cache/utils.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
@ -63,17 +65,17 @@ void CustomTexManager::TickFrame() {
|
||||
return;
|
||||
}
|
||||
std::size_t num_uploads = 0;
|
||||
for (auto it = async_uploads.begin(); it != async_uploads.end();) {
|
||||
for (auto it = async_actions.begin(); it != async_actions.end();) {
|
||||
if (num_uploads >= MAX_UPLOADS_PER_TICK) {
|
||||
return;
|
||||
}
|
||||
switch (it->material->state) {
|
||||
case DecodeState::Decoded:
|
||||
it->func();
|
||||
it->func(it->material);
|
||||
num_uploads++;
|
||||
[[fallthrough]];
|
||||
case DecodeState::Failed:
|
||||
it = async_uploads.erase(it);
|
||||
it = async_actions.erase(it);
|
||||
continue;
|
||||
default:
|
||||
it++;
|
||||
@ -102,7 +104,7 @@ void CustomTexManager::FindCustomTextures() {
|
||||
if (file.isDirectory) {
|
||||
continue;
|
||||
}
|
||||
custom_textures.push_back(std::make_unique<CustomTexture>(image_interface));
|
||||
custom_textures.emplace_back(std::make_unique<CustomTexture>(image_interface));
|
||||
CustomTexture* const texture{custom_textures.back().get()};
|
||||
if (!ParseFilename(file, texture)) {
|
||||
continue;
|
||||
@ -292,16 +294,17 @@ Material* CustomTexManager::GetMaterial(u64 data_hash) {
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
bool CustomTexManager::Decode(Material* material, std::function<bool()>&& upload) {
|
||||
bool CustomTexManager::Decode(Material* material, AsyncFunc&& upload) {
|
||||
if (!async_custom_loading) {
|
||||
material->LoadFromDisk(flip_png_files);
|
||||
return upload();
|
||||
return upload(material);
|
||||
}
|
||||
if (material->IsUnloaded()) {
|
||||
material->state = DecodeState::Pending;
|
||||
workers->QueueWork([material, this] { material->LoadFromDisk(flip_png_files); });
|
||||
}
|
||||
async_uploads.push_back({
|
||||
std::scoped_lock lock{async_actions_mutex};
|
||||
async_actions.push_back({
|
||||
.material = material,
|
||||
.func = std::move(upload),
|
||||
});
|
||||
@ -374,6 +377,14 @@ std::vector<FileUtil::FSTEntry> CustomTexManager::GetTextures(u64 title_id) {
|
||||
FileUtil::CreateFullPath(load_path);
|
||||
}
|
||||
|
||||
const auto callback = [this](const std::string& file, Common::FileAction action) {
|
||||
OnFileAction(file, action);
|
||||
};
|
||||
|
||||
// Create a file watcher to monitor any changes to the textures directory for hot-reloading.
|
||||
file_watcher = std::make_unique<Common::FileWatcher>(load_path, callback);
|
||||
|
||||
// Retrieve all texture files.
|
||||
FileUtil::FSTEntry texture_dir;
|
||||
std::vector<FileUtil::FSTEntry> textures;
|
||||
FileUtil::ScanDirectoryTree(load_path, texture_dir, 64);
|
||||
@ -386,4 +397,24 @@ void CustomTexManager::CreateWorkers() {
|
||||
workers = std::make_unique<Common::ThreadWorker>(num_workers, "Custom textures");
|
||||
}
|
||||
|
||||
void CustomTexManager::OnFileAction(const std::string& file, Common::FileAction action) {
|
||||
const auto invalidate = [this](const Material* material) -> bool {
|
||||
for (const SurfaceParams* params : material->loaded_to) {
|
||||
system.Renderer().Rasterizer()->InvalidateRegion(params->addr, params->size);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const std::string filename{FileUtil::GetFilename(file)};
|
||||
const auto& hashes = path_to_hash_map[filename];
|
||||
std::scoped_lock lock{async_actions_mutex};
|
||||
|
||||
for (const Hash hash : hashes) {
|
||||
async_actions.push_back({
|
||||
.material = material_map[hash].get(),
|
||||
.func = std::move(invalidate),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace VideoCore
|
||||
|
@ -12,6 +12,11 @@
|
||||
#include "video_core/custom_textures/material.h"
|
||||
#include "video_core/rasterizer_interface.h"
|
||||
|
||||
namespace Common {
|
||||
class FileWatcher;
|
||||
enum class FileAction : u8;
|
||||
} // namespace Common
|
||||
|
||||
namespace Core {
|
||||
class System;
|
||||
}
|
||||
@ -24,12 +29,10 @@ namespace VideoCore {
|
||||
|
||||
class SurfaceParams;
|
||||
|
||||
struct AsyncUpload {
|
||||
const Material* material;
|
||||
std::function<bool()> func;
|
||||
};
|
||||
|
||||
class CustomTexManager {
|
||||
using Hash = u64;
|
||||
using AsyncFunc = std::function<bool(const Material*)>;
|
||||
|
||||
public:
|
||||
explicit CustomTexManager(Core::System& system);
|
||||
~CustomTexManager();
|
||||
@ -57,7 +60,7 @@ public:
|
||||
Material* GetMaterial(u64 data_hash);
|
||||
|
||||
/// Decodes the textures in material to a consumable format and uploads it.
|
||||
bool Decode(Material* material, std::function<bool()>&& upload);
|
||||
bool Decode(Material* material, AsyncFunc&& func);
|
||||
|
||||
/// True when mipmap uploads should be skipped (legacy packs only)
|
||||
bool SkipMipmaps() const noexcept {
|
||||
@ -79,15 +82,25 @@ private:
|
||||
/// Creates the thread workers.
|
||||
void CreateWorkers();
|
||||
|
||||
/// Callback for when a custom texture file is modified.
|
||||
void OnFileAction(const std::string& file, Common::FileAction action);
|
||||
|
||||
private:
|
||||
struct AsyncAction {
|
||||
const Material* material;
|
||||
AsyncFunc func;
|
||||
};
|
||||
|
||||
Core::System& system;
|
||||
Frontend::ImageInterface& image_interface;
|
||||
std::unordered_set<u64> dumped_textures;
|
||||
std::unordered_map<u64, std::unique_ptr<Material>> material_map;
|
||||
std::unordered_map<std::string, std::vector<u64>> path_to_hash_map;
|
||||
std::unordered_set<Hash> dumped_textures;
|
||||
std::unordered_map<Hash, std::unique_ptr<Material>> material_map;
|
||||
std::unordered_map<std::string, std::vector<Hash>> path_to_hash_map;
|
||||
std::vector<std::unique_ptr<CustomTexture>> custom_textures;
|
||||
std::list<AsyncUpload> async_uploads;
|
||||
std::mutex async_actions_mutex;
|
||||
std::list<AsyncAction> async_actions;
|
||||
std::unique_ptr<Common::ThreadWorker> workers;
|
||||
std::unique_ptr<Common::FileWatcher> file_watcher;
|
||||
bool textures_loaded{false};
|
||||
bool async_custom_loading{true};
|
||||
bool skip_mipmap{false};
|
||||
|
@ -18,6 +18,8 @@ class ImageInterface;
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
class SurfaceParams;
|
||||
|
||||
enum class MapType : u32 {
|
||||
Color = 0,
|
||||
Normal = 1,
|
||||
@ -72,6 +74,7 @@ struct Material {
|
||||
u64 hash;
|
||||
CustomPixelFormat format;
|
||||
std::array<CustomTexture*, MAX_MAPS> textures;
|
||||
mutable std::vector<SurfaceParams*> loaded_to;
|
||||
std::atomic<DecodeState> state{};
|
||||
|
||||
void LoadFromDisk(bool flip_png) noexcept;
|
||||
|
@ -1043,7 +1043,7 @@ bool RasterizerCache<T>::UploadCustomSurface(SurfaceId surface_id, SurfaceInterv
|
||||
|
||||
surface.flags |= SurfaceFlagBits::Custom;
|
||||
|
||||
const auto upload = [this, level, surface_id, material]() -> bool {
|
||||
const auto upload = [this, level, surface_id](const Material* material) -> bool {
|
||||
ASSERT_MSG(True(slot_surfaces[surface_id].flags & SurfaceFlagBits::Custom),
|
||||
"Surface is not suitable for custom upload, aborting!");
|
||||
if (!slot_surfaces[surface_id].IsCustom()) {
|
||||
|
@ -11,7 +11,18 @@ namespace VideoCore {
|
||||
|
||||
SurfaceBase::SurfaceBase(const SurfaceParams& params) : SurfaceParams{params} {}
|
||||
|
||||
SurfaceBase::~SurfaceBase() = default;
|
||||
SurfaceBase::SurfaceBase(const SurfaceParams& params, const Material* mat)
|
||||
: SurfaceParams{params}, material{mat} {
|
||||
custom_format = material->format;
|
||||
material->loaded_to.push_back(this);
|
||||
}
|
||||
|
||||
SurfaceBase::~SurfaceBase() {
|
||||
if (!material) {
|
||||
return;
|
||||
}
|
||||
std::erase_if(material->loaded_to, [this](SurfaceParams* params) { return params == this; });
|
||||
}
|
||||
|
||||
bool SurfaceBase::CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const {
|
||||
if (type == SurfaceType::Fill && IsRegionValid(fill_interval) &&
|
||||
|
@ -26,7 +26,8 @@ DECLARE_ENUM_FLAG_OPERATORS(SurfaceFlagBits);
|
||||
|
||||
class SurfaceBase : public SurfaceParams {
|
||||
public:
|
||||
SurfaceBase(const SurfaceParams& params);
|
||||
explicit SurfaceBase(const SurfaceParams& params);
|
||||
explicit SurfaceBase(const SurfaceParams& params, const Material* mat);
|
||||
~SurfaceBase();
|
||||
|
||||
/// Returns true when this surface can be used to fill the fill_interval of dest_surface
|
||||
|
@ -333,7 +333,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param
|
||||
|
||||
Surface::Surface(TextureRuntime& runtime, const VideoCore::SurfaceBase& surface,
|
||||
const VideoCore::Material* mat)
|
||||
: SurfaceBase{surface}, tuple{runtime.GetFormatTuple(mat->format)} {
|
||||
: SurfaceBase{surface, mat}, tuple{runtime.GetFormatTuple(mat->format)} {
|
||||
if (mat && !driver->IsCustomFormatSupported(mat->format)) {
|
||||
return;
|
||||
}
|
||||
@ -342,9 +342,6 @@ Surface::Surface(TextureRuntime& runtime, const VideoCore::SurfaceBase& surface,
|
||||
const GLenum target =
|
||||
texture_type == VideoCore::TextureType::CubeMap ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D;
|
||||
|
||||
custom_format = mat->format;
|
||||
material = mat;
|
||||
|
||||
textures[0] = MakeHandle(target, mat->width, mat->height, levels, tuple, DebugName(false));
|
||||
if (res_scale != 1) {
|
||||
textures[1] = MakeHandle(target, mat->width, mat->height, levels, DEFAULT_TUPLE,
|
||||
|
@ -3,10 +3,10 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/file_watcher.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/microprofile.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/texture.h"
|
||||
#include "core/core.h"
|
||||
#include "core/frontend/emu_window.h"
|
||||
#include "core/hw/gpu.h"
|
||||
@ -19,7 +19,6 @@
|
||||
#include "video_core/host_shaders/vulkan_present_frag_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_present_interlaced_frag_spv.h"
|
||||
#include "video_core/host_shaders/vulkan_present_vert_spv.h"
|
||||
#include "vulkan/vulkan_format_traits.hpp"
|
||||
|
||||
#include <vk_mem_alloc.h>
|
||||
|
||||
|
@ -750,7 +750,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param
|
||||
|
||||
Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface,
|
||||
const VideoCore::Material* mat)
|
||||
: SurfaceBase{surface}, runtime{&runtime_}, instance{&runtime_.GetInstance()},
|
||||
: SurfaceBase{surface, mat}, runtime{&runtime_}, instance{&runtime_.GetInstance()},
|
||||
scheduler{&runtime_.GetScheduler()}, traits{instance->GetTraits(mat->format)} {
|
||||
if (!traits.transfer_support) {
|
||||
return;
|
||||
@ -791,9 +791,6 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface
|
||||
vk::PipelineStageFlagBits::eTopOfPipe,
|
||||
vk::DependencyFlagBits::eByRegion, {}, {}, barriers);
|
||||
});
|
||||
|
||||
custom_format = mat->format;
|
||||
material = mat;
|
||||
}
|
||||
|
||||
Surface::~Surface() {
|
||||
|
Reference in New Issue
Block a user