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 commentsmaster
parent
d16dce6d99
commit
06f3c90cfb
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 42dd8aa6ded90b1ec06091522774feff51e83fc5
|
@ -1 +1 @@
|
|||||||
Subproject commit 63779c798237346c2b245c546c40b72a5a5913fe
|
Subproject commit e47e674cd09583ff0503f0f6defd6d23d8b718d3
|
@ -1,44 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#include <lodepng.h>
|
|
||||||
#include "common/file_util.h"
|
|
||||||
#include "common/logging/log.h"
|
|
||||||
#include "jni/lodepng_image_interface.h"
|
|
||||||
|
|
||||||
bool LodePNGImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
|
||||||
const std::string& path) {
|
|
||||||
FileUtil::IOFile file(path, "rb");
|
|
||||||
size_t read_size = file.GetSize();
|
|
||||||
std::vector<u8> in(read_size);
|
|
||||||
if (file.ReadBytes(&in[0], read_size) != read_size) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to decode {}", path);
|
|
||||||
}
|
|
||||||
u32 lodepng_ret = lodepng::decode(dst, width, height, in);
|
|
||||||
if (lodepng_ret) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to decode {} because {}", path,
|
|
||||||
lodepng_error_text(lodepng_ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LodePNGImageInterface::EncodePNG(const std::string& path, const std::vector<u8>& src,
|
|
||||||
u32 width, u32 height) {
|
|
||||||
std::vector<u8> out;
|
|
||||||
u32 lodepng_ret = lodepng::encode(out, src, width, height);
|
|
||||||
if (lodepng_ret) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to encode {} because {}", path,
|
|
||||||
lodepng_error_text(lodepng_ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
FileUtil::IOFile file(path, "wb");
|
|
||||||
if (file.WriteBytes(&out[0], out.size()) != out.size()) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to save encode to path={}", path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "core/frontend/image_interface.h"
|
|
||||||
|
|
||||||
class LodePNGImageInterface final : public Frontend::ImageInterface {
|
|
||||||
public:
|
|
||||||
bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height, const std::string& path) override;
|
|
||||||
bool EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
|
|
||||||
u32 height) override;
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#include <lodepng.h>
|
|
||||||
#include "citra/lodepng_image_interface.h"
|
|
||||||
#include "common/logging/log.h"
|
|
||||||
|
|
||||||
bool LodePNGImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
|
||||||
const std::string& path) {
|
|
||||||
u32 lodepng_ret = lodepng::decode(dst, width, height, path);
|
|
||||||
if (lodepng_ret) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to decode {} because {}", path,
|
|
||||||
lodepng_error_text(lodepng_ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LodePNGImageInterface::EncodePNG(const std::string& path, const std::vector<u8>& src,
|
|
||||||
u32 width, u32 height) {
|
|
||||||
u32 lodepng_ret = lodepng::encode(path, src, width, height);
|
|
||||||
if (lodepng_ret) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to encode {} because {}", path,
|
|
||||||
lodepng_error_text(lodepng_ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "core/frontend/image_interface.h"
|
|
||||||
|
|
||||||
class LodePNGImageInterface final : public Frontend::ImageInterface {
|
|
||||||
public:
|
|
||||||
bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height, const std::string& path) override;
|
|
||||||
bool EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
|
|
||||||
u32 height) override;
|
|
||||||
};
|
|
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "common/error.h"
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
std::string NativeErrorToString(int e) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
LPSTR err_str;
|
||||||
|
|
||||||
|
DWORD res = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
||||||
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
nullptr, e, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||||
|
reinterpret_cast<LPSTR>(&err_str), 1, nullptr);
|
||||||
|
if (!res) {
|
||||||
|
return "(FormatMessageA failed to format error)";
|
||||||
|
}
|
||||||
|
std::string ret(err_str);
|
||||||
|
LocalFree(err_str);
|
||||||
|
return ret;
|
||||||
|
#else
|
||||||
|
char err_str[255];
|
||||||
|
#if defined(__GLIBC__) && (_GNU_SOURCE || (_POSIX_C_SOURCE < 200112L && _XOPEN_SOURCE < 600)) || \
|
||||||
|
defined(ANDROID)
|
||||||
|
// Thread safe (GNU-specific)
|
||||||
|
const char* str = strerror_r(e, err_str, sizeof(err_str));
|
||||||
|
return std::string(str);
|
||||||
|
#else
|
||||||
|
// Thread safe (XSI-compliant)
|
||||||
|
int second_err = strerror_r(e, err_str, sizeof(err_str));
|
||||||
|
if (second_err != 0) {
|
||||||
|
return "(strerror_r failed to format error)";
|
||||||
|
}
|
||||||
|
return std::string(err_str);
|
||||||
|
#endif // GLIBC etc.
|
||||||
|
#endif // _WIN32
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetLastErrorMsg() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return NativeErrorToString(GetLastError());
|
||||||
|
#else
|
||||||
|
return NativeErrorToString(errno);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
// Generic function to get last error message.
|
||||||
|
// Call directly after the command or use the error num.
|
||||||
|
// This function might change the error code.
|
||||||
|
// Defined in error.cpp.
|
||||||
|
[[nodiscard]] std::string GetLastErrorMsg();
|
||||||
|
|
||||||
|
// Like GetLastErrorMsg(), but passing an explicit error code.
|
||||||
|
// Defined in error.cpp.
|
||||||
|
[[nodiscard]] std::string NativeErrorToString(int e);
|
||||||
|
|
||||||
|
} // namespace Common
|
@ -0,0 +1,338 @@
|
|||||||
|
// Copyright 2022 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
//
|
||||||
|
// TODO: remove this file when jthread is supported by all compilation targets
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <version>
|
||||||
|
|
||||||
|
#ifdef __cpp_lib_jthread
|
||||||
|
|
||||||
|
#include <stop_token>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
template <typename Condvar, typename Lock, typename Pred>
|
||||||
|
void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred&& pred) {
|
||||||
|
cv.wait(lock, token, std::move(pred));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace std {
|
||||||
|
namespace polyfill {
|
||||||
|
|
||||||
|
using stop_state_callback = size_t;
|
||||||
|
|
||||||
|
class stop_state {
|
||||||
|
public:
|
||||||
|
stop_state() = default;
|
||||||
|
~stop_state() = default;
|
||||||
|
|
||||||
|
bool request_stop() {
|
||||||
|
unique_lock lk{m_lock};
|
||||||
|
|
||||||
|
if (m_stop_requested) {
|
||||||
|
// Already set, nothing to do.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark stop requested.
|
||||||
|
m_stop_requested = true;
|
||||||
|
|
||||||
|
while (!m_callbacks.empty()) {
|
||||||
|
// Get an iterator to the first element.
|
||||||
|
const auto it = m_callbacks.begin();
|
||||||
|
|
||||||
|
// Move the callback function out of the map.
|
||||||
|
function<void()> f;
|
||||||
|
swap(it->second, f);
|
||||||
|
|
||||||
|
// Erase the now-empty map element.
|
||||||
|
m_callbacks.erase(it);
|
||||||
|
|
||||||
|
// Run the callback.
|
||||||
|
if (f) {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool stop_requested() const {
|
||||||
|
unique_lock lk{m_lock};
|
||||||
|
return m_stop_requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_state_callback insert_callback(function<void()> f) {
|
||||||
|
unique_lock lk{m_lock};
|
||||||
|
|
||||||
|
if (m_stop_requested) {
|
||||||
|
// Stop already requested. Don't insert anything,
|
||||||
|
// just run the callback synchronously.
|
||||||
|
if (f) {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the callback.
|
||||||
|
stop_state_callback ret = ++m_next_callback;
|
||||||
|
m_callbacks.emplace(ret, move(f));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove_callback(stop_state_callback cb) {
|
||||||
|
unique_lock lk{m_lock};
|
||||||
|
m_callbacks.erase(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable recursive_mutex m_lock;
|
||||||
|
map<stop_state_callback, function<void()>> m_callbacks;
|
||||||
|
stop_state_callback m_next_callback{0};
|
||||||
|
bool m_stop_requested{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace polyfill
|
||||||
|
|
||||||
|
#ifndef __cpp_lib_concepts
|
||||||
|
template <class T, class... Args>
|
||||||
|
concept constructible_from = is_nothrow_destructible_v<T> && is_constructible_v<T, Args...>;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class stop_token;
|
||||||
|
class stop_source;
|
||||||
|
struct nostopstate_t {
|
||||||
|
explicit nostopstate_t() = default;
|
||||||
|
};
|
||||||
|
inline constexpr nostopstate_t nostopstate{};
|
||||||
|
|
||||||
|
template <class Callback>
|
||||||
|
class stop_callback;
|
||||||
|
|
||||||
|
class stop_token {
|
||||||
|
public:
|
||||||
|
stop_token() noexcept = default;
|
||||||
|
|
||||||
|
stop_token(const stop_token&) noexcept = default;
|
||||||
|
stop_token(stop_token&&) noexcept = default;
|
||||||
|
stop_token& operator=(const stop_token&) noexcept = default;
|
||||||
|
stop_token& operator=(stop_token&&) noexcept = default;
|
||||||
|
~stop_token() = default;
|
||||||
|
|
||||||
|
void swap(stop_token& other) noexcept {
|
||||||
|
m_stop_state.swap(other.m_stop_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool stop_requested() const noexcept {
|
||||||
|
return m_stop_state && m_stop_state->stop_requested();
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool stop_possible() const noexcept {
|
||||||
|
return m_stop_state != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class stop_source;
|
||||||
|
template <typename Callback>
|
||||||
|
friend class stop_callback;
|
||||||
|
stop_token(shared_ptr<polyfill::stop_state> stop_state) : m_stop_state(move(stop_state)) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
shared_ptr<polyfill::stop_state> m_stop_state;
|
||||||
|
};
|
||||||
|
|
||||||
|
class stop_source {
|
||||||
|
public:
|
||||||
|
stop_source() : m_stop_state(make_shared<polyfill::stop_state>()) {}
|
||||||
|
explicit stop_source(nostopstate_t) noexcept {}
|
||||||
|
|
||||||
|
stop_source(const stop_source&) noexcept = default;
|
||||||
|
stop_source(stop_source&&) noexcept = default;
|
||||||
|
stop_source& operator=(const stop_source&) noexcept = default;
|
||||||
|
stop_source& operator=(stop_source&&) noexcept = default;
|
||||||
|
~stop_source() = default;
|
||||||
|
void swap(stop_source& other) noexcept {
|
||||||
|
m_stop_state.swap(other.m_stop_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] stop_token get_token() const noexcept {
|
||||||
|
return stop_token(m_stop_state);
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool stop_possible() const noexcept {
|
||||||
|
return m_stop_state != nullptr;
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool stop_requested() const noexcept {
|
||||||
|
return m_stop_state && m_stop_state->stop_requested();
|
||||||
|
}
|
||||||
|
bool request_stop() noexcept {
|
||||||
|
return m_stop_state && m_stop_state->request_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class jthread;
|
||||||
|
explicit stop_source(shared_ptr<polyfill::stop_state> stop_state)
|
||||||
|
: m_stop_state(move(stop_state)) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
shared_ptr<polyfill::stop_state> m_stop_state;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Callback>
|
||||||
|
class stop_callback {
|
||||||
|
static_assert(is_nothrow_destructible_v<Callback>);
|
||||||
|
static_assert(is_invocable_v<Callback>);
|
||||||
|
|
||||||
|
public:
|
||||||
|
using callback_type = Callback;
|
||||||
|
|
||||||
|
template <typename C>
|
||||||
|
requires constructible_from<Callback, C>
|
||||||
|
explicit stop_callback(const stop_token& st,
|
||||||
|
C&& cb) noexcept(is_nothrow_constructible_v<Callback, C>)
|
||||||
|
: m_stop_state(st.m_stop_state) {
|
||||||
|
if (m_stop_state) {
|
||||||
|
m_callback = m_stop_state->insert_callback(move(cb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template <typename C>
|
||||||
|
requires constructible_from<Callback, C>
|
||||||
|
explicit stop_callback(stop_token&& st,
|
||||||
|
C&& cb) noexcept(is_nothrow_constructible_v<Callback, C>)
|
||||||
|
: m_stop_state(move(st.m_stop_state)) {
|
||||||
|
if (m_stop_state) {
|
||||||
|
m_callback = m_stop_state->insert_callback(move(cb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
~stop_callback() {
|
||||||
|
if (m_stop_state && m_callback) {
|
||||||
|
m_stop_state->remove_callback(m_callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_callback(const stop_callback&) = delete;
|
||||||
|
stop_callback(stop_callback&&) = delete;
|
||||||
|
stop_callback& operator=(const stop_callback&) = delete;
|
||||||
|
stop_callback& operator=(stop_callback&&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
shared_ptr<polyfill::stop_state> m_stop_state;
|
||||||
|
polyfill::stop_state_callback m_callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Callback>
|
||||||
|
stop_callback(stop_token, Callback) -> stop_callback<Callback>;
|
||||||
|
|
||||||
|
class jthread {
|
||||||
|
public:
|
||||||
|
using id = thread::id;
|
||||||
|
using native_handle_type = thread::native_handle_type;
|
||||||
|
|
||||||
|
jthread() noexcept = default;
|
||||||
|
|
||||||
|
template <typename F, typename... Args,
|
||||||
|
typename = enable_if_t<!is_same_v<remove_cvref_t<F>, jthread>>>
|
||||||
|
explicit jthread(F&& f, Args&&... args)
|
||||||
|
: m_stop_state(make_shared<polyfill::stop_state>()),
|
||||||
|
m_thread(make_thread(move(f), move(args)...)) {}
|
||||||
|
|
||||||
|
~jthread() {
|
||||||
|
if (joinable()) {
|
||||||
|
request_stop();
|
||||||
|
join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jthread(const jthread&) = delete;
|
||||||
|
jthread(jthread&&) noexcept = default;
|
||||||
|
jthread& operator=(const jthread&) = delete;
|
||||||
|
|
||||||
|
jthread& operator=(jthread&& other) noexcept {
|
||||||
|
m_thread.swap(other.m_thread);
|
||||||
|
m_stop_state.swap(other.m_stop_state);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void swap(jthread& other) noexcept {
|
||||||
|
m_thread.swap(other.m_thread);
|
||||||
|
m_stop_state.swap(other.m_stop_state);
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool joinable() const noexcept {
|
||||||
|
return m_thread.joinable();
|
||||||
|
}
|
||||||
|
void join() {
|
||||||
|
m_thread.join();
|
||||||
|
}
|
||||||
|
void detach() {
|
||||||
|
m_thread.detach();
|
||||||
|
m_stop_state.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] id get_id() const noexcept {
|
||||||
|
return m_thread.get_id();
|
||||||
|
}
|
||||||
|
[[nodiscard]] native_handle_type native_handle() {
|
||||||
|
return m_thread.native_handle();
|
||||||
|
}
|
||||||
|
[[nodiscard]] stop_source get_stop_source() noexcept {
|
||||||
|
return stop_source(m_stop_state);
|
||||||
|
}
|
||||||
|
[[nodiscard]] stop_token get_stop_token() const noexcept {
|
||||||
|
return stop_source(m_stop_state).get_token();
|
||||||
|
}
|
||||||
|
bool request_stop() noexcept {
|
||||||
|
return get_stop_source().request_stop();
|
||||||
|
}
|
||||||
|
[[nodiscard]] static unsigned int hardware_concurrency() noexcept {
|
||||||
|
return thread::hardware_concurrency();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename F, typename... Args>
|
||||||
|
thread make_thread(F&& f, Args&&... args) {
|
||||||
|
if constexpr (is_invocable_v<decay_t<F>, stop_token, decay_t<Args>...>) {
|
||||||
|
return thread(move(f), get_stop_token(), move(args)...);
|
||||||
|
} else {
|
||||||
|
return thread(move(f), move(args)...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_ptr<polyfill::stop_state> m_stop_state;
|
||||||
|
thread m_thread;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace std
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
template <typename Condvar, typename Lock, typename Pred>
|
||||||
|
void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred pred) {
|
||||||
|
if (token.stop_requested()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stop_callback callback(token, [&] { cv.notify_all(); });
|
||||||
|
cv.wait(lock, [&] { return pred() || token.stop_requested(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
|
|
||||||
|
#endif // __cpp_lib_jthread
|
@ -0,0 +1,123 @@
|
|||||||
|
// Copyright 2020 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
#include "common/polyfill_thread.h"
|
||||||
|
#include "common/thread.h"
|
||||||
|
#include "common/unique_function.h"
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
template <class StateType = void>
|
||||||
|
class StatefulThreadWorker {
|
||||||
|
static constexpr bool with_state = !std::is_same_v<StateType, void>;
|
||||||
|
|
||||||
|
struct DummyCallable {
|
||||||
|
int operator()(size_t) const noexcept {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using Task =
|
||||||
|
std::conditional_t<with_state, UniqueFunction<void, StateType*>, UniqueFunction<void>>;
|
||||||
|
using StateMaker =
|
||||||
|
std::conditional_t<with_state, std::function<StateType(size_t)>, DummyCallable>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit StatefulThreadWorker(size_t num_workers, std::string_view name, StateMaker func = {})
|
||||||
|
: workers_queued{num_workers}, thread_name{name} {
|
||||||
|
const auto lambda = [this, func](std::stop_token stop_token, size_t index) {
|
||||||
|
Common::SetCurrentThreadName(thread_name.data());
|
||||||
|
{
|
||||||
|
[[maybe_unused]] std::conditional_t<with_state, StateType, int> state{func(index)};
|
||||||
|
while (!stop_token.stop_requested()) {
|
||||||
|
Task task;
|
||||||
|
{
|
||||||
|
std::unique_lock lock{queue_mutex};
|
||||||
|
if (requests.empty()) {
|
||||||
|
wait_condition.notify_all();
|
||||||
|
}
|
||||||
|
Common::CondvarWait(condition, lock, stop_token,
|
||||||
|
[this] { return !requests.empty(); });
|
||||||
|
if (stop_token.stop_requested()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
task = std::move(requests.front());
|
||||||
|
requests.pop();
|
||||||
|
}
|
||||||
|
if constexpr (with_state) {
|
||||||
|
task(&state);
|
||||||
|
} else {
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
++work_done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++workers_stopped;
|
||||||
|
wait_condition.notify_all();
|
||||||
|
};
|
||||||
|
threads.reserve(num_workers);
|
||||||
|
for (size_t i = 0; i < num_workers; ++i) {
|
||||||
|
threads.emplace_back(lambda, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatefulThreadWorker& operator=(const StatefulThreadWorker&) = delete;
|
||||||
|
StatefulThreadWorker(const StatefulThreadWorker&) = delete;
|
||||||
|
|
||||||
|
StatefulThreadWorker& operator=(StatefulThreadWorker&&) = delete;
|
||||||
|
StatefulThreadWorker(StatefulThreadWorker&&) = delete;
|
||||||
|
|
||||||
|
void QueueWork(Task work) {
|
||||||
|
{
|
||||||
|
std::unique_lock lock{queue_mutex};
|
||||||
|
requests.emplace(std::move(work));
|
||||||
|
++work_scheduled;
|
||||||
|
}
|
||||||
|
condition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaitForRequests(std::stop_token stop_token = {}) {
|
||||||
|
std::stop_callback callback(stop_token, [this] {
|
||||||
|
for (auto& thread : threads) {
|
||||||
|
thread.request_stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
std::unique_lock lock{queue_mutex};
|
||||||
|
wait_condition.wait(lock, [this] {
|
||||||
|
return workers_stopped >= workers_queued || work_done >= work_scheduled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t NumWorkers() const noexcept {
|
||||||
|
return threads.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::queue<Task> requests;
|
||||||
|
std::mutex queue_mutex;
|
||||||
|
std::condition_variable_any condition;
|
||||||
|
std::condition_variable wait_condition;
|
||||||
|
std::atomic<size_t> work_scheduled{};
|
||||||
|
std::atomic<size_t> work_done{};
|
||||||
|
std::atomic<size_t> workers_stopped{};
|
||||||
|
std::atomic<size_t> workers_queued{};
|
||||||
|
std::string_view thread_name;
|
||||||
|
std::vector<std::jthread> threads;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ThreadWorker = StatefulThreadWorker<>;
|
||||||
|
|
||||||
|
} // namespace Common
|
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2021 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
/// General purpose function wrapper similar to std::function.
|
||||||
|
/// Unlike std::function, the captured values don't have to be copyable.
|
||||||
|
/// This class can be moved but not copied.
|
||||||
|
template <typename ResultType, typename... Args>
|
||||||
|
class UniqueFunction {
|
||||||
|
class CallableBase {
|
||||||
|
public:
|
||||||
|
virtual ~CallableBase() = default;
|
||||||
|
virtual ResultType operator()(Args&&...) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Functor>
|
||||||
|
class Callable final : public CallableBase {
|
||||||
|
public:
|
||||||
|
Callable(Functor&& functor_) : functor{std::move(functor_)} {}
|
||||||
|
~Callable() override = default;
|
||||||
|
|
||||||
|
ResultType operator()(Args&&... args) override {
|
||||||
|
return functor(std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Functor functor;
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
UniqueFunction() = default;
|
||||||
|
|
||||||
|
template <typename Functor>
|
||||||
|
UniqueFunction(Functor&& functor)
|
||||||
|
: callable{std::make_unique<Callable<Functor>>(std::move(functor))} {}
|
||||||
|
|
||||||
|
UniqueFunction& operator=(UniqueFunction&& rhs) noexcept = default;
|
||||||
|
UniqueFunction(UniqueFunction&& rhs) noexcept = default;
|
||||||
|
|
||||||
|
UniqueFunction& operator=(const UniqueFunction&) = delete;
|
||||||
|
UniqueFunction(const UniqueFunction&) = delete;
|
||||||
|
|
||||||
|
ResultType operator()(Args&&... args) const {
|
||||||
|
return (*callable)(std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit operator bool() const noexcept {
|
||||||
|
return static_cast<bool>(callable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<CallableBase> callable;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Common
|
@ -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
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue