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

@ -0,0 +1,36 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "video_core/custom_textures/custom_format.h"
namespace VideoCore {
std::string_view CustomPixelFormatAsString(CustomPixelFormat format) {
switch (format) {
case CustomPixelFormat::RGBA8:
return "RGBA8";
case CustomPixelFormat::BC1:
return "BC1";
case CustomPixelFormat::BC3:
return "BC3";
case CustomPixelFormat::BC5:
return "BC5";
case CustomPixelFormat::BC7:
return "BC7";
case CustomPixelFormat::ASTC4:
return "ASTC4";
case CustomPixelFormat::ASTC6:
return "ASTC6";
case CustomPixelFormat::ASTC8:
return "ASTC8";
default:
return "NotReal";
}
}
bool IsCustomFormatCompressed(CustomPixelFormat format) {
return format != CustomPixelFormat::RGBA8 && format != CustomPixelFormat::Invalid;
}
} // namespace VideoCore

View file

@ -0,0 +1,36 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <limits>
#include <string_view>
#include "common/common_types.h"
namespace VideoCore {
enum class CustomPixelFormat : u32 {
RGBA8 = 0,
BC1 = 1,
BC3 = 2,
BC5 = 3,
BC7 = 4,
ASTC4 = 5,
ASTC6 = 6,
ASTC8 = 7,
Invalid = std::numeric_limits<u32>::max(),
};
enum class CustomFileFormat : u32 {
None = 0,
PNG = 1,
DDS = 2,
KTX = 3,
};
std::string_view CustomPixelFormatAsString(CustomPixelFormat format);
bool IsCustomFormatCompressed(CustomPixelFormat format);
} // namespace VideoCore

View file

@ -0,0 +1,352 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <json.hpp>
#include "common/file_util.h"
#include "common/memory_detect.h"
#include "common/microprofile.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "common/texture.h"
#include "core/core.h"
#include "core/frontend/image_interface.h"
#include "video_core/custom_textures/custom_tex_manager.h"
#include "video_core/rasterizer_cache/surface_params.h"
namespace VideoCore {
namespace {
MICROPROFILE_DEFINE(CustomTexManager_TickFrame, "CustomTexManager", "TickFrame",
MP_RGB(54, 16, 32));
constexpr std::size_t MAX_UPLOADS_PER_TICK = 16;
bool IsPow2(u32 value) {
return value != 0 && (value & (value - 1)) == 0;
}
CustomFileFormat MakeFileFormat(std::string_view ext) {
if (ext == "png") {
return CustomFileFormat::PNG;
} else if (ext == "dds") {
return CustomFileFormat::DDS;
} else if (ext == "ktx") {
return CustomFileFormat::KTX;
}
return CustomFileFormat::None;
}
MapType MakeMapType(std::string_view ext) {
if (ext == "norm") {
return MapType::Normal;
}
LOG_ERROR(Render, "Unknown material extension {}", ext);
return MapType::Color;
}
} // Anonymous namespace
CustomTexManager::CustomTexManager(Core::System& system_)
: system{system_}, image_interface{*system.GetImageInterface()},
async_custom_loading{Settings::values.async_custom_loading.GetValue()} {}
CustomTexManager::~CustomTexManager() = default;
void CustomTexManager::TickFrame() {
MICROPROFILE_SCOPE(CustomTexManager_TickFrame);
if (!textures_loaded) {
return;
}
std::size_t num_uploads = 0;
for (auto it = async_uploads.begin(); it != async_uploads.end();) {
if (num_uploads >= MAX_UPLOADS_PER_TICK) {
return;
}
switch (it->material->state) {
case DecodeState::Decoded:
it->func();
num_uploads++;
[[fallthrough]];
case DecodeState::Failed:
it = async_uploads.erase(it);
continue;
default:
it++;
break;
}
}
}
void CustomTexManager::FindCustomTextures() {
if (textures_loaded) {
return;
}
if (!workers) {
CreateWorkers();
}
const u64 program_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
const std::string load_path =
fmt::format("{}textures/{:016X}/", GetUserPath(FileUtil::UserPath::LoadDir), program_id);
if (!FileUtil::Exists(load_path)) {
FileUtil::CreateFullPath(load_path);
}
ReadConfig(load_path);
FileUtil::FSTEntry texture_dir;
std::vector<FileUtil::FSTEntry> textures;
FileUtil::ScanDirectoryTree(load_path, texture_dir, 64);
FileUtil::GetAllFilesFromNestedEntries(texture_dir, textures);
custom_textures.reserve(textures.size());
for (const FileUtil::FSTEntry& file : textures) {
if (file.isDirectory) {
continue;
}
custom_textures.push_back(std::make_unique<CustomTexture>(image_interface));
CustomTexture* const texture{custom_textures.back().get()};
if (!ParseFilename(file, texture)) {
continue;
}
auto& material = material_map[texture->hash];
if (!material) {
material = std::make_unique<Material>();
}
material->AddMapTexture(texture);
}
textures_loaded = true;
}
bool CustomTexManager::ParseFilename(const FileUtil::FSTEntry& file, CustomTexture* texture) {
auto parts = Common::SplitString(file.virtualName, '.');
if (parts.size() > 3) {
LOG_ERROR(Render, "Invalid filename {}, ignoring", file.virtualName);
return false;
}
// The last string should always be the file extension.
const CustomFileFormat file_format = MakeFileFormat(parts.back());
if (file_format == CustomFileFormat::None) {
return false;
}
if (file_format == CustomFileFormat::DDS && refuse_dds) {
LOG_ERROR(Render, "Legacy pack is attempting to use DDS textures, skipping!");
return false;
}
texture->file_format = file_format;
parts.pop_back();
// This means the texture is a material type other than color.
texture->type = MapType::Color;
if (parts.size() > 1) {
texture->type = MakeMapType(parts.back());
parts.pop_back();
}
// First check if the path is mapped directly to a hash
// before trying to parse the texture filename.
const auto it = path_to_hash_map.find(file.virtualName);
if (it != path_to_hash_map.end()) {
texture->hash = it->second;
} else {
u32 width;
u32 height;
u32 format;
unsigned long long hash{};
if (std::sscanf(parts.back().c_str(), "tex1_%ux%u_%llX_%u", &width, &height, &hash,
&format) != 4) {
return false;
}
texture->hash = hash;
}
texture->path = file.physicalName;
return true;
}
void CustomTexManager::WriteConfig() {
const u64 program_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
const std::string dump_path =
fmt::format("{}textures/{:016X}/", GetUserPath(FileUtil::UserPath::DumpDir), program_id);
const std::string pack_config = dump_path + "pack.json";
if (FileUtil::Exists(pack_config)) {
return;
}
nlohmann::ordered_json json;
json["author"] = "citra";
json["version"] = "1.0.0";
json["description"] = "A graphics pack";
auto& options = json["options"];
options["skip_mipmap"] = skip_mipmap;
options["flip_png_files"] = flip_png_files;
options["use_new_hash"] = use_new_hash;
FileUtil::IOFile file{pack_config, "w"};
const std::string output = json.dump(4);
file.WriteString(output);
}
void CustomTexManager::PreloadTextures() {
u64 size_sum = 0;
const u64 sys_mem = Common::GetMemInfo().total_physical_memory;
const u64 recommended_min_mem = 2 * size_t(1024 * 1024 * 1024);
// keep 2GB memory for system stability if system RAM is 4GB+ - use half of memory in other
// cases
const u64 max_mem =
(sys_mem / 2 < recommended_min_mem) ? (sys_mem / 2) : (sys_mem - recommended_min_mem);
workers->QueueWork([&]() {
for (auto& [hash, material] : material_map) {
if (size_sum > max_mem) {
LOG_WARNING(Render, "Aborting texture preload due to insufficient memory");
return;
}
material->LoadFromDisk(flip_png_files);
size_sum += material->size;
}
});
workers->WaitForRequests();
async_custom_loading = false;
}
void CustomTexManager::DumpTexture(const SurfaceParams& params, u32 level, std::span<u8> data,
u64 data_hash) {
const u64 program_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
const u32 data_size = static_cast<u32>(data.size());
const u32 width = params.width;
const u32 height = params.height;
const PixelFormat format = params.pixel_format;
std::string dump_path = fmt::format(
"{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id);
if (!FileUtil::CreateFullPath(dump_path)) {
LOG_ERROR(Render, "Unable to create {}", dump_path);
return;
}
dump_path +=
fmt::format("tex1_{}x{}_{:016X}_{}_mip{}.png", width, height, data_hash, format, level);
if (dumped_textures.contains(data_hash) || FileUtil::Exists(dump_path)) {
return;
}
// Make sure the texture size is a power of 2.
// If not, the surface is probably a framebuffer
if (!IsPow2(width) || !IsPow2(height)) {
LOG_WARNING(Render, "Not dumping {:016X} because size isn't a power of 2 ({}x{})",
data_hash, width, height);
return;
}
const u32 decoded_size = width * height * 4;
std::vector<u8> pixels(data_size + decoded_size);
std::memcpy(pixels.data(), data.data(), data_size);
auto dump = [this, width, height, params, data_size, decoded_size, pixels = std::move(pixels),
dump_path = std::move(dump_path)]() mutable {
const std::span encoded = std::span{pixels}.first(data_size);
const std::span decoded = std::span{pixels}.last(decoded_size);
DecodeTexture(params, params.addr, params.end, encoded, decoded,
params.type == SurfaceType::Color);
Common::FlipRGBA8Texture(decoded, width, height);
image_interface.EncodePNG(dump_path, width, height, decoded);
};
if (!workers) {
CreateWorkers();
}
workers->QueueWork(std::move(dump));
dumped_textures.insert(data_hash);
}
Material* CustomTexManager::GetMaterial(u64 data_hash) {
const auto it = material_map.find(data_hash);
if (it == material_map.end()) {
LOG_WARNING(Render, "Unable to find replacement for surface with hash {:016X}", data_hash);
return nullptr;
}
return it->second.get();
}
bool CustomTexManager::Decode(Material* material, std::function<bool()>&& upload) {
if (!async_custom_loading) {
material->LoadFromDisk(flip_png_files);
return upload();
}
if (material->IsUnloaded()) {
material->state = DecodeState::Pending;
workers->QueueWork([material, this] { material->LoadFromDisk(flip_png_files); });
}
async_uploads.push_back({
.material = material,
.func = std::move(upload),
});
return false;
}
void CustomTexManager::ReadConfig(const std::string& load_path) {
const std::string config_path = load_path + "pack.json";
FileUtil::IOFile file{config_path, "r"};
if (!file.IsOpen()) {
LOG_INFO(Render, "Unable to find pack config file, using legacy defaults");
refuse_dds = true;
return;
}
std::string config(file.GetSize(), '\0');
const std::size_t read_size = file.ReadBytes(config.data(), config.size());
if (!read_size) {
return;
}
nlohmann::json json = nlohmann::json::parse(config);
const auto& options = json["options"];
skip_mipmap = options["skip_mipmap"].get<bool>();
flip_png_files = options["flip_png_files"].get<bool>();
use_new_hash = options["use_new_hash"].get<bool>();
refuse_dds = skip_mipmap || !use_new_hash;
const auto& textures = json["textures"];
for (const auto& material : textures.items()) {
size_t idx{};
const u64 hash = std::stoull(material.key(), &idx, 16);
if (!idx) {
LOG_ERROR(Render, "Key {} is invalid, skipping", material.key());
continue;
}
const auto parse = [&](const std::string& file) {
const std::string filename{FileUtil::GetFilename(file)};
auto [it, new_hash] = path_to_hash_map.try_emplace(filename);
if (!new_hash) {
LOG_ERROR(Render,
"File {} with key {} already exists and is mapped to {:#016X}, skipping",
file, material.key(), path_to_hash_map[filename]);
return;
}
it->second = hash;
};
const auto value = material.value();
if (value.is_string()) {
const auto file = value.get<std::string>();
parse(file);
} else if (value.is_array()) {
const auto files = value.get<std::vector<std::string>>();
for (const std::string& file : files) {
parse(file);
}
} else {
LOG_ERROR(Render, "Material with key {} is invalid", material.key());
}
}
}
void CustomTexManager::CreateWorkers() {
const std::size_t num_workers = std::max(std::thread::hardware_concurrency(), 2U) - 1;
workers = std::make_unique<Common::ThreadWorker>(num_workers, "Custom textures");
}
} // namespace VideoCore

View file

@ -0,0 +1,94 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <list>
#include <span>
#include <unordered_map>
#include <unordered_set>
#include "common/thread_worker.h"
#include "video_core/custom_textures/material.h"
namespace Core {
class System;
}
namespace FileUtil {
struct FSTEntry;
}
namespace VideoCore {
class SurfaceParams;
struct AsyncUpload {
const Material* material;
std::function<bool()> func;
};
class CustomTexManager {
public:
explicit CustomTexManager(Core::System& system);
~CustomTexManager();
/// Processes queued texture uploads
void TickFrame();
/// Searches the load directory assigned to program_id for any custom textures and loads them
void FindCustomTextures();
/// Saves the pack configuration file template to the dump directory if it doesn't exist.
void WriteConfig();
/// Preloads all registered custom textures
void PreloadTextures();
/// Saves the provided pixel data described by params to disk as png
void DumpTexture(const SurfaceParams& params, u32 level, std::span<u8> data, u64 data_hash);
/// Returns the material assigned to the provided data hash
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);
/// True when mipmap uploads should be skipped (legacy packs only)
bool SkipMipmaps() const noexcept {
return skip_mipmap;
}
/// Returns true if the pack uses the new hashing method.
bool UseNewHash() const noexcept {
return use_new_hash;
}
private:
/// Parses the custom texture filename (hash, material type, etc).
bool ParseFilename(const FileUtil::FSTEntry& file, CustomTexture* texture);
/// Reads the pack configuration file
void ReadConfig(const std::string& load_path);
/// Creates the thread workers.
void CreateWorkers();
private:
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, u64> path_to_hash_map;
std::vector<std::unique_ptr<CustomTexture>> custom_textures;
std::list<AsyncUpload> async_uploads;
std::unique_ptr<Common::ThreadWorker> workers;
bool textures_loaded{false};
bool async_custom_loading{true};
bool skip_mipmap{true};
bool flip_png_files{true};
bool use_new_hash{false};
bool refuse_dds{false};
};
} // namespace VideoCore

View file

@ -0,0 +1,151 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/texture.h"
#include "core/frontend/image_interface.h"
#include "video_core/custom_textures/material.h"
namespace VideoCore {
namespace {
CustomPixelFormat ToCustomPixelFormat(ddsktx_format format) {
switch (format) {
case DDSKTX_FORMAT_RGBA8:
return CustomPixelFormat::RGBA8;
case DDSKTX_FORMAT_BC1:
return CustomPixelFormat::BC1;
case DDSKTX_FORMAT_BC3:
return CustomPixelFormat::BC3;
case DDSKTX_FORMAT_BC5:
return CustomPixelFormat::BC5;
case DDSKTX_FORMAT_BC7:
return CustomPixelFormat::BC7;
case DDSKTX_FORMAT_ASTC4x4:
return CustomPixelFormat::ASTC4;
case DDSKTX_FORMAT_ASTC6x6:
return CustomPixelFormat::ASTC6;
case DDSKTX_FORMAT_ASTC8x6:
return CustomPixelFormat::ASTC8;
default:
LOG_ERROR(Common, "Unknown dds/ktx pixel format {}", format);
return CustomPixelFormat::RGBA8;
}
}
std::string_view MapTypeName(MapType type) {
switch (type) {
case MapType::Color:
return "Color";
case MapType::Normal:
return "Normal";
default:
return "Invalid";
}
}
} // Anonymous namespace
CustomTexture::CustomTexture(Frontend::ImageInterface& image_interface_)
: image_interface{image_interface_} {}
CustomTexture::~CustomTexture() = default;
void CustomTexture::LoadFromDisk(bool flip_png) {
FileUtil::IOFile file{path, "rb"};
std::vector<u8> input(file.GetSize());
if (file.ReadBytes(input.data(), input.size()) != input.size()) {
LOG_CRITICAL(Render, "Failed to open custom texture: {}", path);
return;
}
switch (file_format) {
case CustomFileFormat::PNG:
LoadPNG(input, flip_png);
break;
case CustomFileFormat::DDS:
case CustomFileFormat::KTX:
LoadDDS(input);
break;
default:
LOG_ERROR(Render, "Unknown file format {}", file_format);
return;
}
}
void CustomTexture::LoadPNG(std::span<const u8> input, bool flip_png) {
if (!image_interface.DecodePNG(data, width, height, input)) {
LOG_ERROR(Render, "Failed to decode png: {}", path);
return;
}
if (flip_png) {
Common::FlipRGBA8Texture(data, width, height);
}
format = CustomPixelFormat::RGBA8;
}
void CustomTexture::LoadDDS(std::span<const u8> input) {
ddsktx_format dds_format{};
image_interface.DecodeDDS(data, width, height, dds_format, input);
format = ToCustomPixelFormat(dds_format);
}
void Material::LoadFromDisk(bool flip_png) noexcept {
if (IsDecoded()) {
return;
}
for (CustomTexture* const texture : textures) {
if (!texture || texture->IsLoaded()) {
continue;
}
texture->LoadFromDisk(flip_png);
size += texture->data.size();
LOG_DEBUG(Render, "Loading {} map {} with hash {:#016X}", MapTypeName(texture->type),
texture->path, texture->hash);
}
if (!textures[0]) {
LOG_ERROR(Render, "Unable to create material without color texture!");
state = DecodeState::Failed;
return;
}
width = textures[0]->width;
height = textures[0]->height;
format = textures[0]->format;
for (const CustomTexture* texture : textures) {
if (!texture) {
continue;
}
if (texture->width != width || texture->height != height) {
LOG_ERROR(Render,
"{} map {} of material with hash {:#016X} has dimentions {}x{} "
"which do not match the color texture dimentions {}x{}",
MapTypeName(texture->type), texture->path, texture->hash, texture->width,
texture->height, width, height);
state = DecodeState::Failed;
return;
}
if (texture->format != format) {
LOG_ERROR(
Render, "{} map {} is stored with {} format which does not match color format {}",
MapTypeName(texture->type), texture->path,
CustomPixelFormatAsString(texture->format), CustomPixelFormatAsString(format));
state = DecodeState::Failed;
return;
}
}
state = DecodeState::Decoded;
}
void Material::AddMapTexture(CustomTexture* texture) noexcept {
const std::size_t index = static_cast<std::size_t>(texture->type);
if (textures[index]) {
LOG_ERROR(Render, "Textures {} and {} are assigned to the same material, ignoring!",
textures[index]->path, texture->path);
return;
}
textures[index] = texture;
}
} // namespace VideoCore

View file

@ -0,0 +1,99 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <atomic>
#include <span>
#include <string>
#include <vector>
#include "video_core/custom_textures/custom_format.h"
namespace Frontend {
class ImageInterface;
}
namespace VideoCore {
enum class MapType : u32 {
Color = 0,
Normal = 1,
MapCount = 2,
};
constexpr std::size_t MAX_MAPS = static_cast<std::size_t>(MapType::MapCount);
enum class DecodeState : u32 {
None = 0,
Pending = 1,
Decoded = 2,
Failed = 3,
};
class CustomTexture {
public:
explicit CustomTexture(Frontend::ImageInterface& image_interface);
~CustomTexture();
void LoadFromDisk(bool flip_png);
[[nodiscard]] bool IsParsed() const noexcept {
return file_format != CustomFileFormat::None && hash != 0;
}
[[nodiscard]] bool IsLoaded() const noexcept {
return !data.empty();
}
private:
void LoadPNG(std::span<const u8> input, bool flip_png);
void LoadDDS(std::span<const u8> input);
public:
Frontend::ImageInterface& image_interface;
std::string path;
u32 width;
u32 height;
u64 hash;
CustomPixelFormat format;
CustomFileFormat file_format;
std::vector<u8> data;
MapType type;
};
struct Material {
u32 width;
u32 height;
u64 size;
CustomPixelFormat format;
std::array<CustomTexture*, MAX_MAPS> textures;
std::atomic<DecodeState> state{};
void LoadFromDisk(bool flip_png) noexcept;
void AddMapTexture(CustomTexture* texture) noexcept;
[[nodiscard]] CustomTexture* Map(MapType type) const noexcept {
return textures.at(static_cast<std::size_t>(type));
}
[[nodiscard]] bool IsPending() const noexcept {
return state == DecodeState::Pending;
}
[[nodiscard]] bool IsFailed() const noexcept {
return state == DecodeState::Failed;
}
[[nodiscard]] bool IsDecoded() const noexcept {
return state == DecodeState::Decoded;
}
[[nodiscard]] bool IsUnloaded() const noexcept {
return state == DecodeState::None;
}
};
} // namespace VideoCore