Custom textures rewrite (#6452)

* common: Add thread pool from yuzu

* Is really useful for asynchronous operations like shader compilation and custom textures, will be used in following PRs

* core: Improve ImageInterface

* Provide a default implementation so frontends don't have to duplicate code registering the lodepng version

* Add a dds version too which we will use in the next commit

* rasterizer_cache: Rewrite custom textures

* There's just too much to talk about here, look at the PR description for more details

* rasterizer_cache: Implement basic pack configuration file

* custom_tex_manager: Flip dumped textures

* custom_tex_manager: Optimize custom texture hashing

* If no convertions are needed then we can hash the decoded data directly removing the needed for duplicate decode

* custom_tex_manager: Implement asynchronous texture loading

* The file loading and decoding is offloaded into worker threads, while the upload itself still occurs in the main thread to avoid having to manage shared contexts

* Address review comments

* custom_tex_manager: Introduce custom material support

* video_core: Move custom textures to separate directory

* Also split the files to make the code cleaner

* gl_texture_runtime: Generate mipmaps for material

* custom_tex_manager: Prevent memory overflow when preloading

* externals: Add dds-ktx as submodule

* string_util: Return vector from SplitString

* No code benefits from passing it as an argument

* custom_textures: Use json config file

* gl_rasterizer: Only bind material for unit 0

* Address review comments
This commit is contained in:
GPUCode 2023-04-27 07:38:28 +03:00 committed by GitHub
parent d16dce6d99
commit 06f3c90cfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 2154 additions and 544 deletions

View file

@ -36,8 +36,6 @@ add_library(core STATIC
core.h
core_timing.cpp
core_timing.h
custom_tex_cache.cpp
custom_tex_cache.h
dumping/backend.cpp
dumping/backend.h
file_sys/archive_backend.cpp
@ -109,6 +107,7 @@ add_library(core STATIC
frontend/emu_window.h
frontend/framebuffer_layout.cpp
frontend/framebuffer_layout.h
frontend/image_interface.cpp
frontend/image_interface.h
frontend/input.h
frontend/mic.cpp
@ -481,7 +480,8 @@ endif()
create_target_directory_groups(core)
target_link_libraries(core PUBLIC common PRIVATE audio_core network video_core)
target_link_libraries(core PUBLIC Boost::boost PRIVATE cryptopp fmt::fmt open_source_archives Boost::serialization Boost::iostreams)
target_link_libraries(core PRIVATE Boost::boost Boost::serialization Boost::iostreams)
target_link_libraries(core PUBLIC dds-ktx PRIVATE cryptopp fmt::fmt lodepng open_source_archives)
set_target_properties(core PROPERTIES INTERPROCEDURAL_OPTIMIZATION ${ENABLE_LTO})
if (ENABLE_WEB_SERVICE)

View file

@ -216,13 +216,12 @@ GatewayCheat::GatewayCheat(std::string name_, std::vector<CheatLine> cheat_lines
GatewayCheat::GatewayCheat(std::string name_, std::string code, std::string comments_)
: name(std::move(name_)), comments(std::move(comments_)) {
std::vector<std::string> code_lines;
Common::SplitString(code, '\n', code_lines);
const auto code_lines = Common::SplitString(code, '\n');
std::vector<CheatLine> temp_cheat_lines;
for (std::size_t i = 0; i < code_lines.size(); ++i) {
if (!code_lines[i].empty())
temp_cheat_lines.emplace_back(code_lines[i]);
for (const std::string& line : code_lines) {
if (!line.empty())
temp_cheat_lines.emplace_back(line);
}
cheat_lines = std::move(temp_cheat_lines);
}
@ -464,10 +463,10 @@ std::string GatewayCheat::ToString() const {
result += EnabledText;
result += '\n';
}
std::vector<std::string> comment_lines;
Common::SplitString(comments, '\n', comment_lines);
for (const auto& comment_line : comment_lines)
const auto comment_lines = Common::SplitString(comments, '\n');
for (const auto& comment_line : comment_lines) {
result += "*" + comment_line + '\n';
}
result += GetCode() + '\n';
return result;
}

View file

@ -27,7 +27,7 @@
#include "core/dumping/ffmpeg_backend.h"
#endif
#include "common/settings.h"
#include "core/custom_tex_cache.h"
#include "core/frontend/image_interface.h"
#include "core/gdbstub/gdbstub.h"
#include "core/global.h"
#include "core/hle/kernel/client_port.h"
@ -48,6 +48,7 @@
#include "core/movie.h"
#include "core/rpc/rpc_server.h"
#include "network/network.h"
#include "video_core/custom_textures/custom_tex_manager.h"
#include "video_core/renderer_base.h"
#include "video_core/video_core.h"
@ -318,16 +319,15 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
static_cast<u32>(load_result));
}
perf_stats = std::make_unique<PerfStats>(title_id);
custom_tex_cache = std::make_unique<Core::CustomTexCache>();
if (Settings::values.custom_textures) {
const u64 program_id = Kernel().GetCurrentProcess()->codeset->program_id;
FileUtil::CreateFullPath(fmt::format(
"{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), program_id));
custom_tex_cache->FindCustomTextures(program_id);
custom_tex_manager->FindCustomTextures();
}
if (Settings::values.preload_textures) {
custom_tex_cache->PreloadTextures(*GetImageInterface());
custom_tex_manager->PreloadTextures();
}
if (Settings::values.dump_textures) {
custom_tex_manager->WriteConfig();
}
status = ResultStatus::Success;
@ -432,6 +432,12 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window,
video_dumper = std::make_unique<VideoDumper::NullBackend>();
#endif
if (!registered_image_interface) {
registered_image_interface = std::make_shared<Frontend::ImageInterface>();
}
custom_tex_manager = std::make_unique<VideoCore::CustomTexManager>(*this);
VideoCore::Init(emu_window, secondary_window, *this);
LOG_DEBUG(Core, "Initialized OK");
@ -505,12 +511,12 @@ const VideoDumper::Backend& System::VideoDumper() const {
return *video_dumper;
}
Core::CustomTexCache& System::CustomTexCache() {
return *custom_tex_cache;
VideoCore::CustomTexManager& System::CustomTexManager() {
return *custom_tex_manager;
}
const Core::CustomTexCache& System::CustomTexCache() const {
return *custom_tex_cache;
const VideoCore::CustomTexManager& System::CustomTexManager() const {
return *custom_tex_manager;
}
void System::RegisterMiiSelector(std::shared_ptr<Frontend::MiiSelector> mii_selector) {

View file

@ -10,10 +10,8 @@
#include <string>
#include <boost/serialization/version.hpp>
#include "common/common_types.h"
#include "core/custom_tex_cache.h"
#include "core/frontend/applets/mii_selector.h"
#include "core/frontend/applets/swkbd.h"
#include "core/frontend/image_interface.h"
#include "core/loader/loader.h"
#include "core/memory.h"
#include "core/perf_stats.h"
@ -23,7 +21,8 @@ class ARM_Interface;
namespace Frontend {
class EmuWindow;
}
class ImageInterface;
} // namespace Frontend
namespace Memory {
class MemorySystem;
@ -59,8 +58,9 @@ class Backend;
}
namespace VideoCore {
class CustomTexManager;
class RendererBase;
}
} // namespace VideoCore
namespace Core {
@ -253,10 +253,10 @@ public:
[[nodiscard]] const Cheats::CheatEngine& CheatEngine() const;
/// Gets a reference to the custom texture cache system
[[nodiscard]] Core::CustomTexCache& CustomTexCache();
[[nodiscard]] VideoCore::CustomTexManager& CustomTexManager();
/// Gets a const reference to the custom texture cache system
[[nodiscard]] const Core::CustomTexCache& CustomTexCache() const;
[[nodiscard]] const VideoCore::CustomTexManager& CustomTexManager() const;
/// Gets a reference to the video dumper backend
[[nodiscard]] VideoDumper::Backend& VideoDumper();
@ -362,7 +362,7 @@ private:
std::unique_ptr<VideoDumper::Backend> video_dumper;
/// Custom texture cache system
std::unique_ptr<Core::CustomTexCache> custom_tex_cache;
std::unique_ptr<VideoCore::CustomTexManager> custom_tex_manager;
/// Image interface
std::shared_ptr<Frontend::ImageInterface> registered_image_interface;

View file

@ -1,109 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <fmt/format.h>
#include "common/file_util.h"
#include "common/texture.h"
#include "core.h"
#include "core/custom_tex_cache.h"
namespace Core {
CustomTexCache::CustomTexCache() = default;
CustomTexCache::~CustomTexCache() = default;
bool CustomTexCache::IsTextureDumped(u64 hash) const {
return dumped_textures.count(hash);
}
void CustomTexCache::SetTextureDumped(const u64 hash) {
dumped_textures.insert(hash);
}
bool CustomTexCache::IsTextureCached(u64 hash) const {
return custom_textures.count(hash);
}
const CustomTexInfo& CustomTexCache::LookupTexture(u64 hash) const {
return custom_textures.at(hash);
}
void CustomTexCache::CacheTexture(u64 hash, const std::vector<u8>& tex, u32 width, u32 height) {
custom_textures[hash] = {width, height, tex};
}
void CustomTexCache::AddTexturePath(u64 hash, const std::string& path) {
if (custom_texture_paths.count(hash))
LOG_ERROR(Core, "Textures {} and {} conflict!", custom_texture_paths[hash].path, path);
else
custom_texture_paths[hash] = {path, hash};
}
void CustomTexCache::FindCustomTextures(u64 program_id) {
// Custom textures are currently stored as
// [TitleID]/tex1_[width]x[height]_[64-bit hash]_[format].png
const std::string load_path = fmt::format(
"{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), program_id);
if (FileUtil::Exists(load_path)) {
FileUtil::FSTEntry texture_dir;
std::vector<FileUtil::FSTEntry> textures;
// 64 nested folders should be plenty for most cases
FileUtil::ScanDirectoryTree(load_path, texture_dir, 64);
FileUtil::GetAllFilesFromNestedEntries(texture_dir, textures);
for (const auto& file : textures) {
if (file.isDirectory)
continue;
if (file.virtualName.substr(0, 5) != "tex1_")
continue;
u32 width;
u32 height;
u64 hash;
u32 format; // unused
// TODO: more modern way of doing this
if (std::sscanf(file.virtualName.c_str(), "tex1_%ux%u_%llX_%u.png", &width, &height,
&hash, &format) == 4) {
AddTexturePath(hash, file.physicalName);
}
}
}
}
void CustomTexCache::PreloadTextures(Frontend::ImageInterface& image_interface) {
for (const auto& path : custom_texture_paths) {
const auto& path_info = path.second;
Core::CustomTexInfo tex_info;
if (image_interface.DecodePNG(tex_info.tex, tex_info.width, tex_info.height,
path_info.path)) {
// Make sure the texture size is a power of 2
std::bitset<32> width_bits(tex_info.width);
std::bitset<32> height_bits(tex_info.height);
if (width_bits.count() == 1 && height_bits.count() == 1) {
LOG_DEBUG(Render_OpenGL, "Loaded custom texture from {}", path_info.path);
Common::FlipRGBA8Texture(tex_info.tex, tex_info.width, tex_info.height);
CacheTexture(path_info.hash, tex_info.tex, tex_info.width, tex_info.height);
} else {
LOG_ERROR(Render_OpenGL, "Texture {} size is not a power of 2", path_info.path);
}
} else {
LOG_ERROR(Render_OpenGL, "Failed to load custom texture {}", path_info.path);
}
}
}
bool CustomTexCache::CustomTextureExists(u64 hash) const {
return custom_texture_paths.count(hash);
}
const CustomTexPathInfo& CustomTexCache::LookupTexturePathInfo(u64 hash) const {
return custom_texture_paths.at(hash);
}
bool CustomTexCache::IsTexturePathMapEmpty() const {
return custom_texture_paths.size() == 0;
}
} // namespace Core

View file

@ -1,55 +0,0 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "common/common_types.h"
namespace Frontend {
class ImageInterface;
} // namespace Frontend
namespace Core {
struct CustomTexInfo {
u32 width;
u32 height;
std::vector<u8> tex;
};
// This is to avoid parsing the filename multiple times
struct CustomTexPathInfo {
std::string path;
u64 hash;
};
// TODO: think of a better name for this class...
class CustomTexCache {
public:
explicit CustomTexCache();
~CustomTexCache();
bool IsTextureDumped(u64 hash) const;
void SetTextureDumped(u64 hash);
bool IsTextureCached(u64 hash) const;
const CustomTexInfo& LookupTexture(u64 hash) const;
void CacheTexture(u64 hash, const std::vector<u8>& tex, u32 width, u32 height);
void AddTexturePath(u64 hash, const std::string& path);
void FindCustomTextures(u64 program_id);
void PreloadTextures(Frontend::ImageInterface& image_interface);
bool CustomTextureExists(u64 hash) const;
const CustomTexPathInfo& LookupTexturePathInfo(u64 hash) const;
bool IsTexturePathMapEmpty() const;
private:
std::unordered_set<u64> dumped_textures;
std::unordered_map<u64, CustomTexInfo> custom_textures;
std::unordered_map<u64, CustomTexPathInfo> custom_texture_paths;
};
} // namespace Core

View file

@ -813,8 +813,7 @@ std::vector<FormatInfo> ListFormats() {
void* data = nullptr; // For libavformat to save the iteration state
while ((current = av_muxer_iterate(&data))) {
#endif
std::vector<std::string> extensions;
Common::SplitString(ToStdString(current->extensions), ',', extensions);
const auto extensions = Common::SplitString(ToStdString(current->extensions), ',');
std::set<AVCodecID> supported_video_codecs;
std::set<AVCodecID> supported_audio_codecs;

View file

@ -32,7 +32,7 @@ PathParser::PathParser(const Path& path) {
return;
}
Common::SplitString(path_string, '/', path_sequence);
path_sequence = Common::SplitString(path_string, '/');
auto begin = path_sequence.begin();
auto end = path_sequence.end();

View file

@ -0,0 +1,65 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#define DDSKTX_IMPLEMENT
#include <dds-ktx.h>
#include <lodepng.h>
#include "common/file_util.h"
#include "common/logging/log.h"
#include "core/frontend/image_interface.h"
namespace Frontend {
bool ImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
std::span<const u8> src) {
const u32 lodepng_ret = lodepng::decode(dst, width, height, src.data(), src.size());
if (lodepng_ret) {
LOG_ERROR(Frontend, "Failed to decode because {}", lodepng_error_text(lodepng_ret));
return false;
}
return true;
}
bool ImageInterface::DecodeDDS(std::vector<u8>& dst, u32& width, u32& height, ddsktx_format& format,
std::span<const u8> src) {
ddsktx_texture_info tc{};
const int size = static_cast<int>(src.size());
if (!ddsktx_parse(&tc, src.data(), size, nullptr)) {
LOG_ERROR(Frontend, "Failed to decode");
return false;
}
width = tc.width;
height = tc.height;
format = tc.format;
ddsktx_sub_data sub_data{};
ddsktx_get_sub(&tc, &sub_data, src.data(), size, 0, 0, 0);
dst.resize(sub_data.size_bytes);
std::memcpy(dst.data(), sub_data.buff, sub_data.size_bytes);
return true;
}
bool ImageInterface::EncodePNG(const std::string& path, u32 width, u32 height,
std::span<const u8> src) {
std::vector<u8> out;
const u32 lodepng_ret = lodepng::encode(out, src.data(), width, height);
if (lodepng_ret) {
LOG_ERROR(Frontend, "Failed to encode {} because {}", path,
lodepng_error_text(lodepng_ret));
return false;
}
FileUtil::IOFile file{path, "wb"};
if (file.WriteBytes(out.data(), out.size()) != out.size()) {
LOG_ERROR(Frontend, "Failed to save encode to path {}", path);
return false;
}
return true;
}
} // namespace Frontend

View file

@ -4,21 +4,26 @@
#pragma once
#include <span>
#include <string>
#include <vector>
#include <dds-ktx.h>
#include "common/common_types.h"
namespace Frontend {
/**
* Utility class that provides image decoding/encoding to the custom texture manager.
* Can be optionally overriden by frontends to provide a custom implementation.
*/
class ImageInterface {
public:
virtual ~ImageInterface() = default;
// Error logging should be handled by the frontend
virtual bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
const std::string& path) = 0;
virtual bool EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
u32 height) = 0;
virtual bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height, std::span<const u8> src);
virtual bool DecodeDDS(std::vector<u8>& dst, u32& width, u32& height, ddsktx_format& format,
std::span<const u8> src);
virtual bool EncodePNG(const std::string& path, u32 width, u32 height, std::span<const u8> src);
};
} // namespace Frontend

View file

@ -136,8 +136,7 @@ void HandleHioReply(const u8* const command_buffer, const u32 command_length) {
}
const std::string command_str{command_pos, command_buffer + command_length};
std::vector<std::string> command_parts;
Common::SplitString(command_str, ',', command_parts);
const auto command_parts = Common::SplitString(command_str, ',');
if (command_parts.empty() || command_parts.size() > 3) {
LOG_WARNING(Debug_GDBStub, "Unexpected reply packet size: {}", command_parts);

View file

@ -458,8 +458,7 @@ void LoadPresetKeys() {
continue;
}
std::vector<std::string> parts;
Common::SplitString(line, '=', parts);
const auto parts = Common::SplitString(line, '=');
if (parts.size() != 2) {
LOG_ERROR(HW_AES, "Failed to parse {}", line);
continue;