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:
parent
d16dce6d99
commit
06f3c90cfb
87 changed files with 2154 additions and 544 deletions
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
65
src/core/frontend/image_interface.cpp
Normal file
65
src/core/frontend/image_interface.cpp
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue