Compare commits

...

3 Commits

Author SHA1 Message Date
aa79505ddd custom_tex_manager: Implement hot-reloading 2023-11-13 10:05:08 +02:00
2b7faf60a3 common: Add FileWatcher 2023-11-11 15:35:11 +02:00
fd32a82b4e rasterizer_cache: Avoid dumping render targets 2023-11-06 23:43:50 +02:00
13 changed files with 277 additions and 47 deletions

View File

@ -89,6 +89,8 @@ add_library(citra_common STATIC
expected.h
file_util.cpp
file_util.h
file_watcher.cpp
file_watcher.h
hash.h
linear_disk_cache.h
literals.h

View File

@ -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);

148
src/common/file_watcher.cpp Normal file
View File

@ -0,0 +1,148 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <windows.h>
#include <thread>
#include "common/assert.h"
#include "common/file_watcher.h"
namespace Common {
static FileAction Win32ActionToFileAction(DWORD action) {
switch (action) {
case FILE_ACTION_ADDED:
return FileAction::Added;
case FILE_ACTION_REMOVED:
return FileAction::Removed;
case FILE_ACTION_MODIFIED:
return FileAction::Modified;
case FILE_ACTION_RENAMED_OLD_NAME:
return FileAction::RenamedOldName;
case FILE_ACTION_RENAMED_NEW_NAME:
return FileAction::RenamedNewName;
default:
UNREACHABLE_MSG("Unknown action {}", action);
return FileAction::Invalid;
}
}
struct FileWatcher::Impl {
explicit Impl(const std::string& path, FileWatcher::Callback&& callback_)
: callback{callback_} {
// Create file handle for the directory we are watching.
dir_handle =
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");
// Create an event that will terminate the thread when fired.
termination_event = CreateEvent(NULL, TRUE, FALSE, NULL);
ASSERT_MSG(termination_event != INVALID_HANDLE_VALUE, "Unable to create watch event");
// Create an event that will wake up the watcher thread on filesystem changes.
overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
ASSERT_MSG(overlapped.hEvent != INVALID_HANDLE_VALUE, "Unable to create watch event");
// Create the watcher thread.
watch_thread = std::thread([this] { WatcherThread(); });
}
~Impl() {
// Signal watcher thread to terminate.
SetEvent(termination_event);
// Wait for said termination.
if (watch_thread.joinable()) {
watch_thread.join();
}
// Close used handles.
CancelIo(dir_handle);
GetOverlappedResult(dir_handle, &overlapped, &num_bytes_read, TRUE);
CloseHandle(termination_event);
CloseHandle(overlapped.hEvent);
}
void WatcherThread() {
const std::array wait_handles{overlapped.hEvent, termination_event};
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());
// Sleep until we receive a file changed notification or a termination event.
switch (
WaitForMultipleObjects(wait_handles.size(), wait_handles.data(), FALSE, INFINITE)) {
case WAIT_OBJECT_0: {
// Retrieve asynchronously the data from ReadDirectoryChangesW.
result = GetOverlappedResult(dir_handle, &overlapped, &num_bytes_read, TRUE);
ASSERT_MSG(result, "Unable to retrieve overlapped result: {}", GetLastErrorMsg());
// Notify about file changes.
NotifyFileChanges();
break;
}
case WAIT_OBJECT_0 + 1:
is_running = false;
break;
case WAIT_FAILED:
UNREACHABLE_MSG("Failed waiting for file watcher events: {}", GetLastErrorMsg());
break;
}
}
}
void NotifyFileChanges() {
// If no data was read we have nothing to do.
if (num_bytes_read == 0) [[unlikely]] {
return;
}
u32 next_entry_offset{};
while (true) {
// Retrieve file notify information.
auto fni = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer.data() + next_entry_offset);
// Call the callback function informing about the change.
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) {
break;
}
// Move to next fni structure.
next_entry_offset += fni->NextEntryOffset;
}
}
private:
static constexpr size_t DirectoryWatcherBufferSize = 4096;
FileWatcher::Callback callback;
HANDLE dir_handle{};
HANDLE termination_event{};
OVERLAPPED overlapped{};
std::array<u8, DirectoryWatcherBufferSize> buffer{};
std::atomic_bool is_running{true};
DWORD num_bytes_read{};
std::thread watch_thread;
};
FileWatcher::FileWatcher(const std::string& log_dir, Callback&& callback)
: impl{std::make_unique<Impl>(log_dir, std::move(callback))} {}
FileWatcher::~FileWatcher() = default;
} // namespace Common

33
src/common/file_watcher.h Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <functional>
#include <string>
namespace Common {
enum class FileAction : u8 {
Added,
Removed,
Modified,
RenamedOldName,
RenamedNewName,
Invalid = std::numeric_limits<u8>::max(),
};
class FileWatcher {
using Callback = std::function<void(const std::string&, FileAction)>;
public:
explicit FileWatcher(const std::string& log_dir, Callback&& callback);
~FileWatcher();
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
} // namespace Common

View File

@ -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

View File

@ -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};

View File

@ -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;

View File

@ -653,7 +653,6 @@ FramebufferHelper<T> RasterizerCache<T>::GetFramebufferSurfaces(bool using_color
static_cast<u32>(std::clamp(viewport_rect.bottom, 0, framebuffer_height)),
};
// get color and depth surfaces
SurfaceParams color_params;
color_params.is_tiled = true;
color_params.res_scale = resolution_scale_factor;
@ -672,14 +671,6 @@ FramebufferHelper<T> RasterizerCache<T>::GetFramebufferSurfaces(bool using_color
auto color_vp_interval = color_params.GetSubRectInterval(viewport_clamped);
auto depth_vp_interval = depth_params.GetSubRectInterval(viewport_clamped);
// Make sure that framebuffers don't overlap if both color and depth are being used
if (using_color_fb && using_depth_fb &&
boost::icl::length(color_vp_interval & depth_vp_interval)) {
LOG_CRITICAL(HW_GPU, "Color and depth framebuffer memory regions overlap; "
"overlapping framebuffers not supported!");
using_depth_fb = false;
}
Common::Rectangle<u32> color_rect{};
SurfaceId color_id{};
u32 color_level{};
@ -713,11 +704,13 @@ FramebufferHelper<T> RasterizerCache<T>::GetFramebufferSurfaces(bool using_color
if (color_id) {
color_level = color_surface->LevelOf(color_params.addr);
color_surface->flags |= SurfaceFlagBits::RenderTarget;
ValidateSurface(color_id, boost::icl::first(color_vp_interval),
boost::icl::length(color_vp_interval));
}
if (depth_id) {
depth_level = depth_surface->LevelOf(depth_params.addr);
depth_surface->flags |= SurfaceFlagBits::RenderTarget;
ValidateSurface(depth_id, boost::icl::first(depth_vp_interval),
boost::icl::length(depth_vp_interval));
}
@ -991,7 +984,9 @@ void RasterizerCache<T>::UploadSurface(Surface& surface, SurfaceInterval interva
DecodeTexture(load_info, load_info.addr, load_info.end, upload_data, staging.mapped,
runtime.NeedsConversion(surface.pixel_format));
if (dump_textures && False(surface.flags & SurfaceFlagBits::Custom)) {
const bool should_dump = False(surface.flags & SurfaceFlagBits::Custom) &&
False(surface.flags & SurfaceFlagBits::RenderTarget);
if (dump_textures && should_dump) {
const u64 hash = ComputeHash(load_info, upload_data);
const u32 level = surface.LevelOf(load_info.addr);
custom_tex_manager.DumpTexture(load_info, level, upload_data, hash);
@ -1048,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()) {

View File

@ -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) &&

View File

@ -20,12 +20,14 @@ enum class SurfaceFlagBits : u32 {
Tracked = 1 << 2, ///< Surface is part of a texture cube and should be tracked.
Custom = 1 << 3, ///< Surface texture has been replaced with a custom texture.
ShadowMap = 1 << 4, ///< Surface is used during shadow rendering.
RenderTarget = 1 << 5, ///< Surface was a render target.
};
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

View File

@ -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,

View File

@ -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>

View File

@ -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() {