From 06f3c90cfb7ffd5e535c779afaa122694fbbed8a Mon Sep 17 00:00:00 2001 From: GPUCode <47210458+GPUCode@users.noreply.github.com> Date: Thu, 27 Apr 2023 07:38:28 +0300 Subject: [PATCH] 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 --- .gitmodules | 3 + externals/CMakeLists.txt | 11 +- externals/dds-ktx | 1 + externals/zstd | 2 +- .../ui/SettingsFragmentPresenter.java | 2 + .../features/settings/utils/SettingsFile.java | 1 + src/android/app/src/main/jni/CMakeLists.txt | 4 +- src/android/app/src/main/jni/cheats/cheat.cpp | 3 +- src/android/app/src/main/jni/config.cpp | 1 + src/android/app/src/main/jni/default_ini.h | 4 + .../src/main/jni/lodepng_image_interface.cpp | 44 --- .../src/main/jni/lodepng_image_interface.h | 14 - src/android/app/src/main/jni/native.cpp | 4 - .../app/src/main/res/values/strings.xml | 2 + src/citra/CMakeLists.txt | 4 +- src/citra/citra.cpp | 4 - src/citra/config.cpp | 1 + src/citra/default_ini.h | 4 + src/citra/lodepng_image_interface.cpp | 29 -- src/citra/lodepng_image_interface.h | 14 - src/citra_qt/configuration/config.cpp | 5 +- src/citra_qt/configuration/config.h | 2 +- .../configuration/configure_enhancements.cpp | 10 + .../configuration/configure_enhancements.h | 1 + .../configuration/configure_enhancements.ui | 10 + src/citra_qt/dumping/option_set_dialog.cpp | 6 +- src/citra_qt/hotkeys.cpp | 2 +- src/citra_qt/hotkeys.h | 4 +- src/citra_qt/main.cpp | 2 + src/citra_qt/qt_image_interface.cpp | 15 +- src/citra_qt/qt_image_interface.h | 6 +- src/common/CMakeLists.txt | 5 + src/common/error.cpp | 57 +++ src/common/error.h | 21 ++ src/common/file_util.cpp | 2 +- src/common/param_package.cpp | 7 +- src/common/polyfill_thread.h | 338 +++++++++++++++++ src/common/settings.h | 1 + src/common/string_util.cpp | 5 +- src/common/string_util.h | 2 +- src/common/texture.cpp | 7 +- src/common/texture.h | 8 +- src/common/thread.cpp | 63 +++- src/common/thread.h | 34 +- src/common/thread_worker.h | 123 ++++++ src/common/unique_function.h | 62 +++ src/core/CMakeLists.txt | 6 +- src/core/cheats/gateway_cheat.cpp | 15 +- src/core/core.cpp | 28 +- src/core/core.h | 14 +- src/core/custom_tex_cache.cpp | 109 ------ src/core/custom_tex_cache.h | 55 --- src/core/dumping/ffmpeg_backend.cpp | 3 +- src/core/file_sys/path_parser.cpp | 2 +- src/core/frontend/image_interface.cpp | 65 ++++ src/core/frontend/image_interface.h | 15 +- src/core/gdbstub/hio.cpp | 3 +- src/core/hw/aes/key.cpp | 3 +- src/video_core/CMakeLists.txt | 8 +- .../custom_textures/custom_format.cpp | 36 ++ .../custom_textures/custom_format.h | 36 ++ .../custom_textures/custom_tex_manager.cpp | 352 ++++++++++++++++++ .../custom_textures/custom_tex_manager.h | 94 +++++ src/video_core/custom_textures/material.cpp | 151 ++++++++ src/video_core/custom_textures/material.h | 99 +++++ .../rasterizer_cache/pixel_format.h | 6 +- .../rasterizer_cache/rasterizer_cache.cpp | 123 ++++-- .../rasterizer_cache/rasterizer_cache.h | 23 +- .../rasterizer_cache/surface_base.cpp | 5 + .../rasterizer_cache/surface_base.h | 11 + .../rasterizer_cache/surface_params.cpp | 6 +- .../rasterizer_cache/surface_params.h | 4 +- .../renderer_opengl/gl_blit_helper.cpp | 31 +- .../renderer_opengl/gl_blit_helper.h | 5 +- src/video_core/renderer_opengl/gl_driver.cpp | 22 ++ src/video_core/renderer_opengl/gl_driver.h | 9 + .../renderer_opengl/gl_rasterizer.cpp | 51 ++- .../renderer_opengl/gl_rasterizer.h | 17 +- .../renderer_opengl/gl_shader_gen.cpp | 14 +- .../renderer_opengl/gl_shader_gen.h | 3 +- .../renderer_opengl/gl_shader_manager.cpp | 5 +- .../renderer_opengl/gl_shader_manager.h | 2 +- src/video_core/renderer_opengl/gl_state.h | 1 + .../renderer_opengl/gl_texture_runtime.cpp | 239 ++++++++---- .../renderer_opengl/gl_texture_runtime.h | 68 ++-- .../renderer_opengl/renderer_opengl.cpp | 8 +- .../renderer_opengl/renderer_opengl.h | 1 - 87 files changed, 2154 insertions(+), 544 deletions(-) create mode 160000 externals/dds-ktx delete mode 100644 src/android/app/src/main/jni/lodepng_image_interface.cpp delete mode 100644 src/android/app/src/main/jni/lodepng_image_interface.h delete mode 100644 src/citra/lodepng_image_interface.cpp delete mode 100644 src/citra/lodepng_image_interface.h create mode 100644 src/common/error.cpp create mode 100644 src/common/error.h create mode 100644 src/common/polyfill_thread.h create mode 100644 src/common/thread_worker.h create mode 100644 src/common/unique_function.h delete mode 100644 src/core/custom_tex_cache.cpp delete mode 100644 src/core/custom_tex_cache.h create mode 100644 src/core/frontend/image_interface.cpp create mode 100644 src/video_core/custom_textures/custom_format.cpp create mode 100644 src/video_core/custom_textures/custom_format.h create mode 100644 src/video_core/custom_textures/custom_tex_manager.cpp create mode 100644 src/video_core/custom_textures/custom_tex_manager.h create mode 100644 src/video_core/custom_textures/material.cpp create mode 100644 src/video_core/custom_textures/material.h diff --git a/.gitmodules b/.gitmodules index dcd856463..faa1cb211 100644 --- a/.gitmodules +++ b/.gitmodules @@ -61,3 +61,6 @@ [submodule "cryptopp"] path = externals/cryptopp url = https://github.com/weidai11/cryptopp.git +[submodule "dds-ktx"] + path = externals/dds-ktx + url = https://github.com/septag/dds-ktx diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index a8401e7b2..74852e300 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -64,6 +64,10 @@ if(ANDROID) endforeach() endif() +# dds-ktx +add_library(dds-ktx INTERFACE) +target_include_directories(dds-ktx INTERFACE ./dds-ktx) + # fmt and Xbyak need to be added before dynarmic # libfmt option(FMT_INSTALL "" ON) @@ -141,6 +145,10 @@ if (USE_DISCORD_PRESENCE) target_include_directories(discord-rpc INTERFACE ./discord-rpc/include) endif() +# JSON +add_library(json-headers INTERFACE) +target_include_directories(json-headers INTERFACE ./json) + if (ENABLE_WEB_SERVICE) find_package(OpenSSL 1.1) if (OPENSSL_FOUND) @@ -156,9 +164,6 @@ if (ENABLE_WEB_SERVICE) DIRECTORY libressl DEFINITION OPENSSL_LIBS) endif() - # JSON - add_library(json-headers INTERFACE) - target_include_directories(json-headers INTERFACE ./json) if(ANDROID) add_subdirectory(android-ifaddrs) diff --git a/externals/dds-ktx b/externals/dds-ktx new file mode 160000 index 000000000..42dd8aa6d --- /dev/null +++ b/externals/dds-ktx @@ -0,0 +1 @@ +Subproject commit 42dd8aa6ded90b1ec06091522774feff51e83fc5 diff --git a/externals/zstd b/externals/zstd index 63779c798..e47e674cd 160000 --- a/externals/zstd +++ b/externals/zstd @@ -1 +1 @@ -Subproject commit 63779c798237346c2b245c546c40b72a5a5913fe +Subproject commit e47e674cd09583ff0503f0f6defd6d23d8b718d3 diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java index 52d838265..cf3ec3fdf 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java @@ -368,6 +368,7 @@ public final class SettingsFragmentPresenter { SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY); Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES); Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES); + Setting asyncCustomLoading = utilitySection.getSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING); //Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES); sl.add(new HeaderSetting(null, null, R.string.renderer, 0)); @@ -389,6 +390,7 @@ public final class SettingsFragmentPresenter { sl.add(new HeaderSetting(null, null, R.string.utility, 0)); sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures)); sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING, Settings.SECTION_UTILITY, R.string.async_custom_loading, R.string.async_custom_loading_description, true, asyncCustomLoading)); //Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra. //sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures)); } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java index ab815d441..54d0121fe 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java @@ -73,6 +73,7 @@ public final class SettingsFile { public static final String KEY_DUMP_TEXTURES = "dump_textures"; public static final String KEY_CUSTOM_TEXTURES = "custom_textures"; public static final String KEY_PRELOAD_TEXTURES = "preload_textures"; + public static final String KEY_ASYNC_CUSTOM_LOADING = "async_custom_loading"; public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine"; public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching"; diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index bb382dc7a..6cc0cf906 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -25,8 +25,6 @@ add_library(citra-android SHARED game_settings.h id_cache.cpp id_cache.h - lodepng_image_interface.cpp - lodepng_image_interface.h mic.cpp mic.h native.cpp @@ -36,6 +34,6 @@ add_library(citra-android SHARED ) target_link_libraries(citra-android PRIVATE audio_core common core input_common network) -target_link_libraries(citra-android PRIVATE android camera2ndk EGL glad inih jnigraphics lodepng log mediandk yuv) +target_link_libraries(citra-android PRIVATE android camera2ndk EGL glad inih jnigraphics log mediandk yuv) set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} citra-android) diff --git a/src/android/app/src/main/jni/cheats/cheat.cpp b/src/android/app/src/main/jni/cheats/cheat.cpp index 3d93ab890..5a5c2c1e9 100644 --- a/src/android/app/src/main/jni/cheats/cheat.cpp +++ b/src/android/app/src/main/jni/cheats/cheat.cpp @@ -62,8 +62,7 @@ JNIEXPORT void JNICALL Java_org_citra_citra_1emu_features_cheats_model_Cheat_set JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_features_cheats_model_Cheat_isValidGatewayCode( JNIEnv* env, jclass, jstring j_code) { const std::string code = GetJString(env, j_code); - std::vector code_lines; - Common::SplitString(code, '\n', code_lines); + const auto code_lines = Common::SplitString(code, '\n'); for (int i = 0; i < code_lines.size(); ++i) { Cheats::GatewayCheat::CheatLine cheat_line(code_lines[i]); diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index c5ea67319..e56e3cf0f 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -195,6 +195,7 @@ void Config::ReadValues() { ReadSetting("Utility", Settings::values.dump_textures); ReadSetting("Utility", Settings::values.custom_textures); ReadSetting("Utility", Settings::values.preload_textures); + ReadSetting("Utility", Settings::values.async_custom_loading); // Audio ReadSetting("Audio", Settings::values.audio_emulation); diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 04c205c85..5ddc1cdfd 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -213,6 +213,10 @@ custom_textures = # 0 (default): Off, 1: On preload_textures = +# Loads custom textures asynchronously with background threads. +# 0: Off, 1 (default): On +async_custom_loading = + [Audio] # Whether or not to enable DSP LLE # 0 (default): No, 1: Yes diff --git a/src/android/app/src/main/jni/lodepng_image_interface.cpp b/src/android/app/src/main/jni/lodepng_image_interface.cpp deleted file mode 100644 index 6bfcb81f3..000000000 --- a/src/android/app/src/main/jni/lodepng_image_interface.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include "common/file_util.h" -#include "common/logging/log.h" -#include "jni/lodepng_image_interface.h" - -bool LodePNGImageInterface::DecodePNG(std::vector& dst, u32& width, u32& height, - const std::string& path) { - FileUtil::IOFile file(path, "rb"); - size_t read_size = file.GetSize(); - std::vector 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& src, - u32 width, u32 height) { - std::vector 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; -} diff --git a/src/android/app/src/main/jni/lodepng_image_interface.h b/src/android/app/src/main/jni/lodepng_image_interface.h deleted file mode 100644 index 6880b10a0..000000000 --- a/src/android/app/src/main/jni/lodepng_image_interface.h +++ /dev/null @@ -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& dst, u32& width, u32& height, const std::string& path) override; - bool EncodePNG(const std::string& path, const std::vector& src, u32 width, - u32 height) override; -}; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index e1f8cc1fc..663e4a83a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -39,7 +39,6 @@ #include "jni/game_settings.h" #include "jni/id_cache.h" #include "jni/input_manager.h" -#include "jni/lodepng_image_interface.h" #include "jni/mic.h" #include "jni/native.h" #include "jni/ndk_motion.h" @@ -184,9 +183,6 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { system.RegisterMiiSelector(std::make_shared()); system.RegisterSoftwareKeyboard(std::make_shared()); - // Register generic image interface - Core::System::GetInstance().RegisterImageInterface(std::make_shared()); - // Register real Mic factory Frontend::Mic::RegisterRealMicFactory(std::make_unique()); diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index add6fe47e..ba47472c6 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -116,6 +116,8 @@ Uses custom textures found in load/textures/[GAME ID] Preload custom textures Loads all custom textures into memory. This feature can use a lot of memory. + Async custom texture loading + Loads custom textures in the background with worker threads to reduce loading stutter. Premium Upgrade to Premium and support Citra! diff --git a/src/citra/CMakeLists.txt b/src/citra/CMakeLists.txt index 03006f984..650ba3162 100644 --- a/src/citra/CMakeLists.txt +++ b/src/citra/CMakeLists.txt @@ -12,8 +12,6 @@ add_executable(citra emu_window/emu_window_sdl2_gl.h emu_window/emu_window_sdl2_sw.cpp emu_window/emu_window_sdl2_sw.h - lodepng_image_interface.cpp - lodepng_image_interface.h precompiled_headers.h resource.h ) @@ -21,7 +19,7 @@ add_executable(citra create_target_directory_groups(citra) target_link_libraries(citra PRIVATE common core input_common network) -target_link_libraries(citra PRIVATE inih glad lodepng) +target_link_libraries(citra PRIVATE inih glad) if (MSVC) target_link_libraries(citra PRIVATE getopt) endif() diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index f8ab9c55a..66c5a5fcf 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -15,7 +15,6 @@ #include "citra/emu_window/emu_window_sdl2.h" #include "citra/emu_window/emu_window_sdl2_gl.h" #include "citra/emu_window/emu_window_sdl2_sw.h" -#include "citra/lodepng_image_interface.h" #include "common/common_paths.h" #include "common/detached_tasks.h" #include "common/file_util.h" @@ -359,9 +358,6 @@ int main(int argc, char** argv) { // Register frontend applets Frontend::RegisterDefaultApplets(); - // Register generic image interface - Core::System::GetInstance().RegisterImageInterface(std::make_shared()); - EmuWindow_SDL2::InitializeSDL2(); const auto create_emu_window = [](bool fullscreen, diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 0e2addff9..12633ba98 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -175,6 +175,7 @@ void Config::ReadValues() { ReadSetting("Utility", Settings::values.dump_textures); ReadSetting("Utility", Settings::values.custom_textures); ReadSetting("Utility", Settings::values.preload_textures); + ReadSetting("Utility", Settings::values.async_custom_loading); // Audio ReadSetting("Audio", Settings::values.audio_emulation); diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index d5636bcd5..e6f020b4d 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -232,6 +232,10 @@ custom_textures = # 0 (default): Off, 1: On preload_textures = +# Loads custom textures asynchronously with background threads. +# 0: Off, 1 (default): On +async_custom_loading = + [Audio] # Whether or not to enable DSP LLE # 0 (default): No, 1: Yes diff --git a/src/citra/lodepng_image_interface.cpp b/src/citra/lodepng_image_interface.cpp deleted file mode 100644 index e537de4e4..000000000 --- a/src/citra/lodepng_image_interface.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include "citra/lodepng_image_interface.h" -#include "common/logging/log.h" - -bool LodePNGImageInterface::DecodePNG(std::vector& 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& 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; -} diff --git a/src/citra/lodepng_image_interface.h b/src/citra/lodepng_image_interface.h deleted file mode 100644 index 6880b10a0..000000000 --- a/src/citra/lodepng_image_interface.h +++ /dev/null @@ -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& dst, u32& width, u32& height, const std::string& path) override; - bool EncodePNG(const std::string& path, const std::vector& src, u32 width, - u32 height) override; -}; diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index ac17f8b77..09b354b31 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -56,7 +56,7 @@ const std::array, Settings::NativeAnalog::NumAnalogs> Config: // This must be in alphabetical order according to action name as it must have the same order as // UISetting::values.shortcuts, which is alphabetically ordered. // clang-format off -const std::array Config::default_hotkeys {{ +const std::array Config::default_hotkeys {{ {QStringLiteral("Advance Frame"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}}, {QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}}, {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}}, @@ -84,6 +84,7 @@ const std::array Config::default_hotkeys {{ {QStringLiteral("Toggle Screen Layout"), QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::WindowShortcut}}, {QStringLiteral("Toggle Status Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+S"), Qt::WindowShortcut}}, {QStringLiteral("Toggle Texture Dumping"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Toggle Custom Textures"), QStringLiteral("Main Window"), {QStringLiteral("F7"), Qt::ApplicationShortcut}}, }}; // clang-format on @@ -439,6 +440,7 @@ void Config::ReadUtilityValues() { ReadGlobalSetting(Settings::values.dump_textures); ReadGlobalSetting(Settings::values.custom_textures); ReadGlobalSetting(Settings::values.preload_textures); + ReadGlobalSetting(Settings::values.async_custom_loading); qt_config->endGroup(); } @@ -949,6 +951,7 @@ void Config::SaveUtilityValues() { WriteGlobalSetting(Settings::values.dump_textures); WriteGlobalSetting(Settings::values.custom_textures); WriteGlobalSetting(Settings::values.preload_textures); + WriteGlobalSetting(Settings::values.async_custom_loading); qt_config->endGroup(); } diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 67eaa40a3..572745055 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -26,7 +26,7 @@ public: static const std::array default_buttons; static const std::array, Settings::NativeAnalog::NumAnalogs> default_analogs; - static const std::array default_hotkeys; + static const std::array default_hotkeys; private: void Initialize(const std::string& config_name); diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp index 996c59976..91e45caee 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -41,8 +41,10 @@ ConfigureEnhancements::ConfigureEnhancements(QWidget* parent) }); ui->toggle_preload_textures->setEnabled(ui->toggle_custom_textures->isChecked()); + ui->toggle_async_custom_loading->setEnabled(ui->toggle_custom_textures->isChecked()); connect(ui->toggle_custom_textures, &QCheckBox::toggled, this, [this] { ui->toggle_preload_textures->setEnabled(ui->toggle_custom_textures->isChecked()); + ui->toggle_async_custom_loading->setEnabled(ui->toggle_custom_textures->isChecked()); if (!ui->toggle_preload_textures->isEnabled()) ui->toggle_preload_textures->setChecked(false); }); @@ -83,6 +85,7 @@ void ConfigureEnhancements::SetConfiguration() { ui->toggle_dump_textures->setChecked(Settings::values.dump_textures.GetValue()); ui->toggle_custom_textures->setChecked(Settings::values.custom_textures.GetValue()); ui->toggle_preload_textures->setChecked(Settings::values.preload_textures.GetValue()); + ui->toggle_async_custom_loading->setChecked(Settings::values.async_custom_loading.GetValue()); bg_color = QColor::fromRgbF(Settings::values.bg_red.GetValue(), Settings::values.bg_green.GetValue(), Settings::values.bg_blue.GetValue()); @@ -159,6 +162,8 @@ void ConfigureEnhancements::ApplyConfiguration() { ui->toggle_custom_textures, custom_textures); ConfigurationShared::ApplyPerGameSetting(&Settings::values.preload_textures, ui->toggle_preload_textures, preload_textures); + ConfigurationShared::ApplyPerGameSetting(&Settings::values.async_custom_loading, + ui->toggle_async_custom_loading, async_custom_loading); Settings::values.bg_red = static_cast(bg_color.redF()); Settings::values.bg_green = static_cast(bg_color.greenF()); @@ -176,6 +181,8 @@ void ConfigureEnhancements::SetupPerGameUI() { ui->toggle_dump_textures->setEnabled(Settings::values.dump_textures.UsingGlobal()); ui->toggle_custom_textures->setEnabled(Settings::values.custom_textures.UsingGlobal()); ui->toggle_preload_textures->setEnabled(Settings::values.preload_textures.UsingGlobal()); + ui->toggle_async_custom_loading->setEnabled( + Settings::values.async_custom_loading.UsingGlobal()); return; } @@ -195,6 +202,9 @@ void ConfigureEnhancements::SetupPerGameUI() { Settings::values.custom_textures, custom_textures); ConfigurationShared::SetColoredTristate(ui->toggle_preload_textures, Settings::values.preload_textures, preload_textures); + ConfigurationShared::SetColoredTristate(ui->toggle_async_custom_loading, + Settings::values.async_custom_loading, + async_custom_loading); ConfigurationShared::SetColoredComboBox( ui->resolution_factor_combobox, ui->widget_resolution, diff --git a/src/citra_qt/configuration/configure_enhancements.h b/src/citra_qt/configuration/configure_enhancements.h index 2bd5d644a..c7b7425d2 100644 --- a/src/citra_qt/configuration/configure_enhancements.h +++ b/src/citra_qt/configuration/configure_enhancements.h @@ -44,5 +44,6 @@ private: ConfigurationShared::CheckState dump_textures; ConfigurationShared::CheckState custom_textures; ConfigurationShared::CheckState preload_textures; + ConfigurationShared::CheckState async_custom_loading; QColor bg_color; }; diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui index 5d1f8aeda..9d224870e 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -494,6 +494,16 @@ + + + + <html><head/><body><p>Load custom textures asynchronously with background threads to reduce loading stutter</p></body></html> + + + Async Custom Texture Loading + + + diff --git a/src/citra_qt/dumping/option_set_dialog.cpp b/src/citra_qt/dumping/option_set_dialog.cpp index ee62c73c1..eaa85655d 100644 --- a/src/citra_qt/dumping/option_set_dialog.cpp +++ b/src/citra_qt/dumping/option_set_dialog.cpp @@ -206,13 +206,11 @@ void OptionSetDialog::SetCheckBoxDefaults(const std::string& initial_value) { } } else { // This is a combination of constants, splitted with + or | - std::vector tmp; - Common::SplitString(initial_value, '+', tmp); + const auto tmp = Common::SplitString(initial_value, '+'); std::vector out; - std::vector tmp2; for (const auto& str : tmp) { - Common::SplitString(str, '|', tmp2); + const auto tmp2 = Common::SplitString(str, '|'); out.insert(out.end(), tmp2.begin(), tmp2.end()); } for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { diff --git a/src/citra_qt/hotkeys.cpp b/src/citra_qt/hotkeys.cpp index 0e5368517..89f5c809a 100644 --- a/src/citra_qt/hotkeys.cpp +++ b/src/citra_qt/hotkeys.cpp @@ -2,13 +2,13 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include #include #include #include "citra_qt/hotkeys.h" #include "citra_qt/uisettings.h" HotkeyRegistry::HotkeyRegistry() = default; + HotkeyRegistry::~HotkeyRegistry() = default; void HotkeyRegistry::SaveHotkeys() { diff --git a/src/citra_qt/hotkeys.h b/src/citra_qt/hotkeys.h index 6a377f511..85f453529 100644 --- a/src/citra_qt/hotkeys.h +++ b/src/citra_qt/hotkeys.h @@ -5,11 +5,13 @@ #pragma once #include +#include +#include class QDialog; -class QKeySequence; class QSettings; class QShortcut; +class QWidget; class HotkeyRegistry final { public: diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 35e6aa23a..066152778 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -578,6 +578,8 @@ void GMainWindow::InitializeHotkeys() { }); connect_shortcut(QStringLiteral("Toggle Texture Dumping"), [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); + connect_shortcut(QStringLiteral("Toggle Custom Textures"), + [&] { Settings::values.custom_textures = !Settings::values.custom_textures; }); // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes // the variable hold a garbage value after this function exits static constexpr u16 SPEED_LIMIT_STEP = 5; diff --git a/src/citra_qt/qt_image_interface.cpp b/src/citra_qt/qt_image_interface.cpp index 00a5e1384..f60162e08 100644 --- a/src/citra_qt/qt_image_interface.cpp +++ b/src/citra_qt/qt_image_interface.cpp @@ -8,11 +8,10 @@ #include "common/logging/log.h" bool QtImageInterface::DecodePNG(std::vector& dst, u32& width, u32& height, - const std::string& path) { - QImage image(QString::fromStdString(path)); - + std::span src) { + QImage image(QImage::fromData(src.data(), static_cast(src.size()))); if (image.isNull()) { - LOG_ERROR(Frontend, "Failed to open {} for decoding", path); + LOG_ERROR(Frontend, "Failed to decode png because image is null"); return false; } width = image.width(); @@ -21,13 +20,15 @@ bool QtImageInterface::DecodePNG(std::vector& dst, u32& width, u32& height, image = image.convertToFormat(QImage::Format_RGBA8888); // Write RGBA8 to vector - dst = std::vector(image.constBits(), image.constBits() + (width * height * 4)); + const size_t image_size = width * height * 4; + dst.resize(image_size); + std::memcpy(dst.data(), image.constBits(), image_size); return true; } -bool QtImageInterface::EncodePNG(const std::string& path, const std::vector& src, u32 width, - u32 height) { +bool QtImageInterface::EncodePNG(const std::string& path, u32 width, u32 height, + std::span src) { QImage image(src.data(), width, height, QImage::Format_RGBA8888); if (!image.save(QString::fromStdString(path), "PNG")) { diff --git a/src/citra_qt/qt_image_interface.h b/src/citra_qt/qt_image_interface.h index 53b49b7b8..e85f84a43 100644 --- a/src/citra_qt/qt_image_interface.h +++ b/src/citra_qt/qt_image_interface.h @@ -8,7 +8,7 @@ class QtImageInterface final : public Frontend::ImageInterface { public: - bool DecodePNG(std::vector& dst, u32& width, u32& height, const std::string& path) override; - bool EncodePNG(const std::string& path, const std::vector& src, u32 width, - u32 height) override; + bool DecodePNG(std::vector& dst, u32& width, u32& height, std::span src) override; + bool EncodePNG(const std::string& path, u32 width, u32 height, + std::span src) override; }; diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index b3635a747..767f68d5a 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -65,6 +65,8 @@ add_library(common STATIC common_precompiled_headers.h common_types.h construct.h + error.cpp + error.h file_util.cpp file_util.h hash.h @@ -89,6 +91,7 @@ add_library(common STATIC misc.cpp param_package.cpp param_package.h + polyfill_thread.h precompiled_headers.h quaternion.h ring_buffer.h @@ -113,9 +116,11 @@ add_library(common STATIC thread.cpp thread.h thread_queue_list.h + thread_worker.h threadsafe_queue.h timer.cpp timer.h + unique_function.h vector_math.h web_result.h x64/cpu_detect.cpp diff --git a/src/common/error.cpp b/src/common/error.cpp new file mode 100644 index 000000000..1d7467f68 --- /dev/null +++ b/src/common/error.cpp @@ -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 +#ifdef _WIN32 +#include +#else +#include +#include +#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(&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 diff --git a/src/common/error.h b/src/common/error.h new file mode 100644 index 000000000..e084d4b0f --- /dev/null +++ b/src/common/error.h @@ -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 + +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 diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 5155b19a4..90970392e 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -960,7 +960,7 @@ std::string_view GetFilename(std::string_view path) { const auto name_index = path.find_last_of("\\/"); if (name_index == std::string_view::npos) { - return {}; + return path; } return path.substr(name_index + 1); diff --git a/src/common/param_package.cpp b/src/common/param_package.cpp index 3a218efbc..bbdde251f 100644 --- a/src/common/param_package.cpp +++ b/src/common/param_package.cpp @@ -28,12 +28,9 @@ ParamPackage::ParamPackage(const std::string& serialized) { return; } - std::vector pairs; - Common::SplitString(serialized, PARAM_SEPARATOR, pairs); - + const auto pairs = Common::SplitString(serialized, PARAM_SEPARATOR); for (const std::string& pair : pairs) { - std::vector key_value; - Common::SplitString(pair, KEY_VALUE_SEPARATOR, key_value); + auto key_value = Common::SplitString(pair, KEY_VALUE_SEPARATOR); if (key_value.size() != 2) { LOG_ERROR(Common, "invalid key pair {}", pair); continue; diff --git a/src/common/polyfill_thread.h b/src/common/polyfill_thread.h new file mode 100644 index 000000000..a4f4abab8 --- /dev/null +++ b/src/common/polyfill_thread.h @@ -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 + +#ifdef __cpp_lib_jthread + +#include +#include + +namespace Common { + +template +void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred&& pred) { + cv.wait(lock, token, std::move(pred)); +} + +} // namespace Common + +#else + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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> m_callbacks; + stop_state_callback m_next_callback{0}; + bool m_stop_requested{false}; +}; + +} // namespace polyfill + +#ifndef __cpp_lib_concepts +template +concept constructible_from = is_nothrow_destructible_v && is_constructible_v; +#endif + +class stop_token; +class stop_source; +struct nostopstate_t { + explicit nostopstate_t() = default; +}; +inline constexpr nostopstate_t nostopstate{}; + +template +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 + friend class stop_callback; + stop_token(shared_ptr stop_state) : m_stop_state(move(stop_state)) {} + +private: + shared_ptr m_stop_state; +}; + +class stop_source { +public: + stop_source() : m_stop_state(make_shared()) {} + 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 stop_state) + : m_stop_state(move(stop_state)) {} + +private: + shared_ptr m_stop_state; +}; + +template +class stop_callback { + static_assert(is_nothrow_destructible_v); + static_assert(is_invocable_v); + +public: + using callback_type = Callback; + + template + requires constructible_from + explicit stop_callback(const stop_token& st, + C&& cb) noexcept(is_nothrow_constructible_v) + : m_stop_state(st.m_stop_state) { + if (m_stop_state) { + m_callback = m_stop_state->insert_callback(move(cb)); + } + } + template + requires constructible_from + explicit stop_callback(stop_token&& st, + C&& cb) noexcept(is_nothrow_constructible_v) + : 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 m_stop_state; + polyfill::stop_state_callback m_callback; +}; + +template +stop_callback(stop_token, Callback) -> stop_callback; + +class jthread { +public: + using id = thread::id; + using native_handle_type = thread::native_handle_type; + + jthread() noexcept = default; + + template , jthread>>> + explicit jthread(F&& f, Args&&... args) + : m_stop_state(make_shared()), + 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 + thread make_thread(F&& f, Args&&... args) { + if constexpr (is_invocable_v, stop_token, decay_t...>) { + return thread(move(f), get_stop_token(), move(args)...); + } else { + return thread(move(f), move(args)...); + } + } + + shared_ptr m_stop_state; + thread m_thread; +}; + +} // namespace std + +namespace Common { + +template +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 diff --git a/src/common/settings.h b/src/common/settings.h index 31f15ee2d..14cd53734 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -477,6 +477,7 @@ struct Values { SwitchableSetting dump_textures{false, "dump_textures"}; SwitchableSetting custom_textures{false, "custom_textures"}; SwitchableSetting preload_textures{false, "preload_textures"}; + SwitchableSetting async_custom_loading{true, "async_custom_loading"}; // Audio bool audio_muted; diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index 3904b7e8c..7ec81e856 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -102,15 +102,16 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P _CompleteFilename += _Filename; } -void SplitString(const std::string& str, const char delim, std::vector& output) { +std::vector SplitString(const std::string& str, const char delim) { std::istringstream iss(str); - output.resize(1); + std::vector output(1); while (std::getline(iss, *output.rbegin(), delim)) { output.emplace_back(); } output.pop_back(); + return output; } std::string TabsToSpaces(int tab_size, std::string in) { diff --git a/src/common/string_util.h b/src/common/string_util.h index 8d3d08d7c..812abdf77 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -29,7 +29,7 @@ namespace Common { [[nodiscard]] bool EndsWith(const std::string& value, const std::string& ending); -void SplitString(const std::string& str, char delim, std::vector& output); +[[nodiscard]] std::vector SplitString(const std::string& str, const char delim); // "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe" bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _pFilename, diff --git a/src/common/texture.cpp b/src/common/texture.cpp index 75b6a2cee..206683bc7 100644 --- a/src/common/texture.cpp +++ b/src/common/texture.cpp @@ -3,12 +3,12 @@ // Refer to the license.txt file included. #include -#include #include "common/assert.h" -#include "common/common_types.h" +#include "common/texture.h" namespace Common { -void FlipRGBA8Texture(std::vector& tex, u32 width, u32 height) { + +void FlipRGBA8Texture(std::span tex, u32 width, u32 height) { ASSERT(tex.size() == width * height * 4); const u32 line_size = width * 4; for (u32 line = 0; line < height / 2; line++) { @@ -19,4 +19,5 @@ void FlipRGBA8Texture(std::vector& tex, u32 width, u32 height) { tex.begin() + offset_2); } } + } // namespace Common diff --git a/src/common/texture.h b/src/common/texture.h index de53433b4..567e4d73d 100644 --- a/src/common/texture.h +++ b/src/common/texture.h @@ -4,9 +4,11 @@ #pragma once -#include +#include #include "common/common_types.h" namespace Common { -void FlipRGBA8Texture(std::vector& tex, u32 width, u32 height); -} + +void FlipRGBA8Texture(std::span tex, u32 width, u32 height); + +} // namespace Common diff --git a/src/common/thread.cpp b/src/common/thread.cpp index 394d19345..119514ab1 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -2,7 +2,9 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include "common/common_funcs.h" +#include + +#include "common/error.h" #include "common/logging/log.h" #include "common/thread.h" #ifdef __APPLE__ @@ -20,7 +22,6 @@ #ifndef _WIN32 #include #endif -#include #ifdef __FreeBSD__ #define cpu_set_t cpuset_t @@ -28,6 +29,56 @@ namespace Common { +#ifdef _WIN32 + +void SetCurrentThreadPriority(ThreadPriority new_priority) { + auto handle = GetCurrentThread(); + int windows_priority = 0; + switch (new_priority) { + case ThreadPriority::Low: + windows_priority = THREAD_PRIORITY_BELOW_NORMAL; + break; + case ThreadPriority::Normal: + windows_priority = THREAD_PRIORITY_NORMAL; + break; + case ThreadPriority::High: + windows_priority = THREAD_PRIORITY_ABOVE_NORMAL; + break; + case ThreadPriority::VeryHigh: + windows_priority = THREAD_PRIORITY_HIGHEST; + break; + case ThreadPriority::Critical: + windows_priority = THREAD_PRIORITY_TIME_CRITICAL; + break; + default: + windows_priority = THREAD_PRIORITY_NORMAL; + break; + } + SetThreadPriority(handle, windows_priority); +} + +#else + +void SetCurrentThreadPriority(ThreadPriority new_priority) { + pthread_t this_thread = pthread_self(); + + const auto scheduling_type = SCHED_OTHER; + s32 max_prio = sched_get_priority_max(scheduling_type); + s32 min_prio = sched_get_priority_min(scheduling_type); + u32 level = std::max(static_cast(new_priority) + 1, 4U); + + struct sched_param params; + if (max_prio > min_prio) { + params.sched_priority = min_prio + ((max_prio - min_prio) * level) / 4; + } else { + params.sched_priority = min_prio - ((min_prio - max_prio) * level) / 4; + } + + pthread_setschedparam(this_thread, scheduling_type, ¶ms); +} + +#endif + #ifdef _MSC_VER // Sets the debugger-visible name of the current thread. @@ -47,7 +98,7 @@ void SetCurrentThreadName(const char* name) { info.dwType = 0x1000; info.szName = name; - info.dwThreadID = static_cast(-1); + info.dwThreadID = std::numeric_limits::max(); info.dwFlags = 0; __try { @@ -81,6 +132,12 @@ void SetCurrentThreadName(const char* name) { } #endif +#if defined(_WIN32) +void SetCurrentThreadName(const char*) { + // Do Nothing on MingW +} +#endif + #endif } // namespace Common diff --git a/src/common/thread.h b/src/common/thread.h index b4881fd44..44d3703f2 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -10,13 +10,15 @@ #include #include #include +#include "common/common_types.h" +#include "common/polyfill_thread.h" namespace Common { class Event { public: void Set() { - std::lock_guard lk{mutex}; + std::scoped_lock lk{mutex}; if (!is_set) { is_set = true; condvar.notify_one(); @@ -54,6 +56,10 @@ public: is_set = false; } + [[nodiscard]] bool IsSet() { + return is_set; + } + private: std::condition_variable condvar; std::mutex mutex; @@ -65,7 +71,7 @@ public: explicit Barrier(std::size_t count_) : count(count_) {} /// Blocks until all "count" threads have called Sync() - void Sync() { + bool Sync(std::stop_token token = {}) { std::unique_lock lk{mutex}; const std::size_t current_generation = generation; @@ -73,25 +79,37 @@ public: generation++; waiting = 0; condvar.notify_all(); + return true; } else { - condvar.wait(lk, - [this, current_generation] { return current_generation != generation; }); + CondvarWait(condvar, lk, token, + [this, current_generation] { return current_generation != generation; }); + return !token.stop_requested(); } } - std::size_t Generation() const { - std::unique_lock lk(mutex); + std::size_t Generation() { + std::unique_lock lk{mutex}; return generation; } private: - std::condition_variable condvar; - mutable std::mutex mutex; + std::condition_variable_any condvar; + std::mutex mutex; std::size_t count; std::size_t waiting = 0; std::size_t generation = 0; // Incremented once each time the barrier is used }; +enum class ThreadPriority : u32 { + Low = 0, + Normal = 1, + High = 2, + VeryHigh = 3, + Critical = 4, +}; + +void SetCurrentThreadPriority(ThreadPriority new_priority); + void SetCurrentThreadName(const char* name); } // namespace Common diff --git a/src/common/thread_worker.h b/src/common/thread_worker.h new file mode 100644 index 000000000..02a9a893d --- /dev/null +++ b/src/common/thread_worker.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/polyfill_thread.h" +#include "common/thread.h" +#include "common/unique_function.h" + +namespace Common { + +template +class StatefulThreadWorker { + static constexpr bool with_state = !std::is_same_v; + + struct DummyCallable { + int operator()(size_t) const noexcept { + return 0; + } + }; + + using Task = + std::conditional_t, UniqueFunction>; + using StateMaker = + std::conditional_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 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 requests; + std::mutex queue_mutex; + std::condition_variable_any condition; + std::condition_variable wait_condition; + std::atomic work_scheduled{}; + std::atomic work_done{}; + std::atomic workers_stopped{}; + std::atomic workers_queued{}; + std::string_view thread_name; + std::vector threads; +}; + +using ThreadWorker = StatefulThreadWorker<>; + +} // namespace Common diff --git a/src/common/unique_function.h b/src/common/unique_function.h new file mode 100644 index 000000000..044c4da6d --- /dev/null +++ b/src/common/unique_function.h @@ -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 +#include + +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 +class UniqueFunction { + class CallableBase { + public: + virtual ~CallableBase() = default; + virtual ResultType operator()(Args&&...) = 0; + }; + + template + 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)...); + } + + private: + Functor functor; + }; + +public: + UniqueFunction() = default; + + template + UniqueFunction(Functor&& functor) + : callable{std::make_unique>(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)...); + } + + explicit operator bool() const noexcept { + return static_cast(callable); + } + +private: + std::unique_ptr callable; +}; + +} // namespace Common diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5b8eabe1b..4bf11d14d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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) diff --git a/src/core/cheats/gateway_cheat.cpp b/src/core/cheats/gateway_cheat.cpp index bb9965af6..1067b5718 100644 --- a/src/core/cheats/gateway_cheat.cpp +++ b/src/core/cheats/gateway_cheat.cpp @@ -216,13 +216,12 @@ GatewayCheat::GatewayCheat(std::string name_, std::vector cheat_lines GatewayCheat::GatewayCheat(std::string name_, std::string code, std::string comments_) : name(std::move(name_)), comments(std::move(comments_)) { - std::vector code_lines; - Common::SplitString(code, '\n', code_lines); + const auto code_lines = Common::SplitString(code, '\n'); std::vector 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 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; } diff --git a/src/core/core.cpp b/src/core/core.cpp index 97650b620..b1dc1622f 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -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(load_result)); } perf_stats = std::make_unique(title_id); - custom_tex_cache = std::make_unique(); 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(); #endif + if (!registered_image_interface) { + registered_image_interface = std::make_shared(); + } + + custom_tex_manager = std::make_unique(*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 mii_selector) { diff --git a/src/core/core.h b/src/core/core.h index d5b82b98e..4468f709c 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -10,10 +10,8 @@ #include #include #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 video_dumper; /// Custom texture cache system - std::unique_ptr custom_tex_cache; + std::unique_ptr custom_tex_manager; /// Image interface std::shared_ptr registered_image_interface; diff --git a/src/core/custom_tex_cache.cpp b/src/core/custom_tex_cache.cpp deleted file mode 100644 index 9045df836..000000000 --- a/src/core/custom_tex_cache.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#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& 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 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 diff --git a/src/core/custom_tex_cache.h b/src/core/custom_tex_cache.h deleted file mode 100644 index d23ad7b91..000000000 --- a/src/core/custom_tex_cache.h +++ /dev/null @@ -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 -#include -#include -#include -#include "common/common_types.h" - -namespace Frontend { -class ImageInterface; -} // namespace Frontend - -namespace Core { -struct CustomTexInfo { - u32 width; - u32 height; - std::vector 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& 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 dumped_textures; - std::unordered_map custom_textures; - std::unordered_map custom_texture_paths; -}; -} // namespace Core diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 5f38d02fd..6c506f043 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -813,8 +813,7 @@ std::vector ListFormats() { void* data = nullptr; // For libavformat to save the iteration state while ((current = av_muxer_iterate(&data))) { #endif - std::vector extensions; - Common::SplitString(ToStdString(current->extensions), ',', extensions); + const auto extensions = Common::SplitString(ToStdString(current->extensions), ','); std::set supported_video_codecs; std::set supported_audio_codecs; diff --git a/src/core/file_sys/path_parser.cpp b/src/core/file_sys/path_parser.cpp index 2daadfbb7..168521f72 100644 --- a/src/core/file_sys/path_parser.cpp +++ b/src/core/file_sys/path_parser.cpp @@ -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(); diff --git a/src/core/frontend/image_interface.cpp b/src/core/frontend/image_interface.cpp new file mode 100644 index 000000000..90b5a6352 --- /dev/null +++ b/src/core/frontend/image_interface.cpp @@ -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 +#include +#include "common/file_util.h" +#include "common/logging/log.h" +#include "core/frontend/image_interface.h" + +namespace Frontend { + +bool ImageInterface::DecodePNG(std::vector& dst, u32& width, u32& height, + std::span 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& dst, u32& width, u32& height, ddsktx_format& format, + std::span src) { + ddsktx_texture_info tc{}; + const int size = static_cast(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 src) { + std::vector 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 diff --git a/src/core/frontend/image_interface.h b/src/core/frontend/image_interface.h index 7febe3331..c6d35e809 100644 --- a/src/core/frontend/image_interface.h +++ b/src/core/frontend/image_interface.h @@ -4,21 +4,26 @@ #pragma once +#include #include #include +#include #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& dst, u32& width, u32& height, - const std::string& path) = 0; - virtual bool EncodePNG(const std::string& path, const std::vector& src, u32 width, - u32 height) = 0; + virtual bool DecodePNG(std::vector& dst, u32& width, u32& height, std::span src); + virtual bool DecodeDDS(std::vector& dst, u32& width, u32& height, ddsktx_format& format, + std::span src); + virtual bool EncodePNG(const std::string& path, u32 width, u32 height, std::span src); }; } // namespace Frontend diff --git a/src/core/gdbstub/hio.cpp b/src/core/gdbstub/hio.cpp index ac085d5b7..970225f31 100644 --- a/src/core/gdbstub/hio.cpp +++ b/src/core/gdbstub/hio.cpp @@ -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 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); diff --git a/src/core/hw/aes/key.cpp b/src/core/hw/aes/key.cpp index 95e09ac86..810a3a27b 100644 --- a/src/core/hw/aes/key.cpp +++ b/src/core/hw/aes/key.cpp @@ -458,8 +458,7 @@ void LoadPresetKeys() { continue; } - std::vector parts; - Common::SplitString(line, '=', parts); + const auto parts = Common::SplitString(line, '='); if (parts.size() != 2) { LOG_ERROR(HW_AES, "Failed to parse {}", line); continue; diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 463465d59..d249f96de 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -3,6 +3,12 @@ add_subdirectory(host_shaders) add_library(video_core STATIC command_processor.cpp command_processor.h + custom_textures/custom_format.cpp + custom_textures/custom_format.h + custom_textures/custom_tex_manager.cpp + custom_textures/custom_tex_manager.h + custom_textures/material.cpp + custom_textures/material.h debug_utils/debug_utils.cpp debug_utils/debug_utils.h geometry_pipeline.cpp @@ -120,7 +126,7 @@ target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE}) create_target_directory_groups(video_core) target_link_libraries(video_core PUBLIC common core) -target_link_libraries(video_core PRIVATE glad nihstro-headers Boost::serialization) +target_link_libraries(video_core PRIVATE glad json-headers dds-ktx nihstro-headers Boost::serialization) set_target_properties(video_core PROPERTIES INTERPROCEDURAL_OPTIMIZATION ${ENABLE_LTO}) if ("x86_64" IN_LIST ARCHITECTURE) diff --git a/src/video_core/custom_textures/custom_format.cpp b/src/video_core/custom_textures/custom_format.cpp new file mode 100644 index 000000000..b165e7a05 --- /dev/null +++ b/src/video_core/custom_textures/custom_format.cpp @@ -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 diff --git a/src/video_core/custom_textures/custom_format.h b/src/video_core/custom_textures/custom_format.h new file mode 100644 index 000000000..c288d88e0 --- /dev/null +++ b/src/video_core/custom_textures/custom_format.h @@ -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 +#include +#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::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 diff --git a/src/video_core/custom_textures/custom_tex_manager.cpp b/src/video_core/custom_textures/custom_tex_manager.cpp new file mode 100644 index 000000000..62962892d --- /dev/null +++ b/src/video_core/custom_textures/custom_tex_manager.cpp @@ -0,0 +1,352 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#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 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(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->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 data, + u64 data_hash) { + const u64 program_id = system.Kernel().GetCurrentProcess()->codeset->program_id; + const u32 data_size = static_cast(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 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&& 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(); + flip_png_files = options["flip_png_files"].get(); + use_new_hash = options["use_new_hash"].get(); + 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(); + parse(file); + } else if (value.is_array()) { + const auto files = value.get>(); + 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(num_workers, "Custom textures"); +} + +} // namespace VideoCore diff --git a/src/video_core/custom_textures/custom_tex_manager.h b/src/video_core/custom_textures/custom_tex_manager.h new file mode 100644 index 000000000..caee66a1f --- /dev/null +++ b/src/video_core/custom_textures/custom_tex_manager.h @@ -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 +#include +#include +#include +#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 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 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&& 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 dumped_textures; + std::unordered_map> material_map; + std::unordered_map path_to_hash_map; + std::vector> custom_textures; + std::list async_uploads; + std::unique_ptr 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 diff --git a/src/video_core/custom_textures/material.cpp b/src/video_core/custom_textures/material.cpp new file mode 100644 index 000000000..9d76c20e7 --- /dev/null +++ b/src/video_core/custom_textures/material.cpp @@ -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 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 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 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(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 diff --git a/src/video_core/custom_textures/material.h b/src/video_core/custom_textures/material.h new file mode 100644 index 000000000..6c43695d0 --- /dev/null +++ b/src/video_core/custom_textures/material.h @@ -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 +#include +#include +#include +#include +#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(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 input, bool flip_png); + + void LoadDDS(std::span input); + +public: + Frontend::ImageInterface& image_interface; + std::string path; + u32 width; + u32 height; + u64 hash; + CustomPixelFormat format; + CustomFileFormat file_format; + std::vector data; + MapType type; +}; + +struct Material { + u32 width; + u32 height; + u64 size; + CustomPixelFormat format; + std::array textures; + std::atomic state{}; + + void LoadFromDisk(bool flip_png) noexcept; + + void AddMapTexture(CustomTexture* texture) noexcept; + + [[nodiscard]] CustomTexture* Map(MapType type) const noexcept { + return textures.at(static_cast(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 diff --git a/src/video_core/rasterizer_cache/pixel_format.h b/src/video_core/rasterizer_cache/pixel_format.h index 34aa08ff3..449bde069 100644 --- a/src/video_core/rasterizer_cache/pixel_format.h +++ b/src/video_core/rasterizer_cache/pixel_format.h @@ -44,7 +44,7 @@ enum class SurfaceType : u32 { Invalid = 5, }; -enum class TextureType : u32 { +enum class TextureType : u16 { Texture2D = 0, CubeMap = 1, }; @@ -99,10 +99,10 @@ constexpr SurfaceType GetFormatType(PixelFormat format) { return FORMAT_MAP[index].type; } -std::string_view PixelFormatAsString(PixelFormat format); - bool CheckFormatsBlittable(PixelFormat source_format, PixelFormat dest_format); +std::string_view PixelFormatAsString(PixelFormat format); + PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format); PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format); diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.cpp b/src/video_core/rasterizer_cache/rasterizer_cache.cpp index c9464cc63..319a5263c 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.cpp +++ b/src/video_core/rasterizer_cache/rasterizer_cache.cpp @@ -8,6 +8,7 @@ #include "common/logging/log.h" #include "common/microprofile.h" #include "core/memory.h" +#include "video_core/custom_textures/custom_tex_manager.h" #include "video_core/rasterizer_cache/rasterizer_cache.h" #include "video_core/regs.h" #include "video_core/renderer_base.h" @@ -19,6 +20,15 @@ namespace { MICROPROFILE_DEFINE(RasterizerCache_CopySurface, "RasterizerCache", "CopySurface", MP_RGB(128, 192, 64)); +MICROPROFILE_DEFINE(RasterizerCache_UploadSurface, "RasterizerCache", "UploadSurface", + MP_RGB(128, 192, 64)); +MICROPROFILE_DEFINE(RasterizerCache_ComputeHash, "RasterizerCache", "ComputeHash", + MP_RGB(32, 64, 192)); +MICROPROFILE_DEFINE(RasterizerCache_DownloadSurface, "RasterizerCache", "DownloadSurface", + MP_RGB(128, 192, 64)); +MICROPROFILE_DEFINE(RasterizerCache_Invalidation, "RasterizerCache", "Invalidation", + MP_RGB(128, 64, 192)); +MICROPROFILE_DEFINE(RasterizerCache_Flush, "RasterizerCache", "Flush", MP_RGB(128, 64, 192)); constexpr auto RangeFromInterval(const auto& map, const auto& interval) { return boost::make_iterator_range(map.equal_range(interval)); @@ -119,11 +129,15 @@ auto FindMatch(const auto& surface_cache, const SurfaceParams& params, ScaleMatc } // Anonymous namespace -RasterizerCache::RasterizerCache(Memory::MemorySystem& memory_, OpenGL::TextureRuntime& runtime_, - Pica::Regs& regs_, RendererBase& renderer_) - : memory{memory_}, runtime{runtime_}, regs{regs_}, renderer{renderer_}, - resolution_scale_factor{renderer.GetResolutionScaleFactor()}, - use_filter{Settings::values.texture_filter.GetValue() != Settings::TextureFilter::None} {} +RasterizerCache::RasterizerCache(Memory::MemorySystem& memory_, + CustomTexManager& custom_tex_manager_, + OpenGL::TextureRuntime& runtime_, Pica::Regs& regs_, + RendererBase& renderer_) + : memory{memory_}, custom_tex_manager{custom_tex_manager_}, runtime{runtime_}, regs{regs_}, + renderer{renderer_}, resolution_scale_factor{renderer.GetResolutionScaleFactor()}, + use_filter{Settings::values.texture_filter.GetValue() != Settings::TextureFilter::None}, + dump_textures{Settings::values.dump_textures.GetValue()}, + use_custom_textures{Settings::values.custom_textures.GetValue()} {} RasterizerCache::~RasterizerCache() { #ifndef ANDROID @@ -132,6 +146,32 @@ RasterizerCache::~RasterizerCache() { #endif } +void RasterizerCache::TickFrame() { + custom_tex_manager.TickFrame(); + + const u32 scale_factor = renderer.GetResolutionScaleFactor(); + const bool resolution_scale_changed = resolution_scale_factor != scale_factor; + const bool use_custom_texture_changed = + Settings::values.custom_textures.GetValue() != use_custom_textures; + const bool texture_filter_changed = + renderer.Settings().texture_filter_update_requested.exchange(false); + + if (resolution_scale_changed || texture_filter_changed || use_custom_texture_changed) { + resolution_scale_factor = scale_factor; + use_filter = Settings::values.texture_filter.GetValue() != Settings::TextureFilter::None; + use_custom_textures = Settings::values.custom_textures.GetValue(); + if (use_custom_textures) { + custom_tex_manager.FindCustomTextures(); + } + FlushAll(); + while (!surface_cache.empty()) { + UnregisterSurface(*surface_cache.begin()->second.begin()); + } + texture_cube_cache.clear(); + runtime.Reset(); + } +} + bool RasterizerCache::AccelerateTextureCopy(const GPU::Regs::DisplayTransferConfig& config) { // Texture copy size is aligned to 16 byte units const u32 copy_size = Common::AlignDown(config.texture_copy.size, 16); @@ -572,21 +612,6 @@ OpenGL::Framebuffer RasterizerCache::GetFramebufferSurfaces(bool using_color_fb, bool using_depth_fb) { const auto& config = regs.framebuffer.framebuffer; - // Update resolution_scale_factor and reset cache if changed - const u32 scale_factor = renderer.GetResolutionScaleFactor(); - const bool resolution_scale_changed = resolution_scale_factor != scale_factor; - const bool texture_filter_changed = - renderer.Settings().texture_filter_update_requested.exchange(false); - - if (resolution_scale_changed || texture_filter_changed) { - resolution_scale_factor = scale_factor; - use_filter = Settings::values.texture_filter.GetValue() != Settings::TextureFilter::None; - FlushAll(); - while (!surface_cache.empty()) - UnregisterSurface(*surface_cache.begin()->second.begin()); - texture_cube_cache.clear(); - } - const s32 framebuffer_width = config.GetWidth(); const s32 framebuffer_height = config.GetHeight(); const auto viewport_rect = regs.rasterizer.GetViewportRect(); @@ -818,11 +843,13 @@ void RasterizerCache::ValidateSurface(const SurfaceRef& surface, PAddr addr, u32 // Filtered mipmaps often look really bad. We can achieve better quality by // generating them from the base level. if (surface->res_scale != 1 && level != 0) { - runtime.GenerateMipmaps(*surface, surface->levels - 1); + runtime.GenerateMipmaps(*surface); } } void RasterizerCache::UploadSurface(const SurfaceRef& surface, SurfaceInterval interval) { + MICROPROFILE_SCOPE(RasterizerCache_UploadSurface); + const SurfaceParams load_info = surface->FromInterval(interval); ASSERT(load_info.addr >= surface->addr && load_info.end <= surface->end); @@ -838,6 +865,18 @@ void RasterizerCache::UploadSurface(const SurfaceRef& surface, SurfaceInterval i DecodeTexture(load_info, load_info.addr, load_info.end, upload_data, staging.mapped, runtime.NeedsConversion(surface->pixel_format)); + if (use_custom_textures) { + const u64 hash = ComputeCustomHash(load_info, staging.mapped, upload_data); + if (UploadCustomSurface(surface, load_info, hash)) { + return; + } + } + if (dump_textures && !surface->is_custom) { + const u64 hash = Common::ComputeHash64(upload_data.data(), upload_data.size()); + const u32 level = surface->LevelOf(load_info.addr); + custom_tex_manager.DumpTexture(load_info, level, upload_data, hash); + } + const BufferTextureCopy upload = { .buffer_offset = 0, .buffer_size = staging.size, @@ -847,7 +886,49 @@ void RasterizerCache::UploadSurface(const SurfaceRef& surface, SurfaceInterval i surface->Upload(upload, staging); } +bool RasterizerCache::UploadCustomSurface(const SurfaceRef& surface, const SurfaceParams& load_info, + u64 hash) { + const u32 level = surface->LevelOf(load_info.addr); + const bool is_base_level = level == 0; + Material* material = custom_tex_manager.GetMaterial(hash); + + if (!material) { + return surface->IsCustom(); + } + if (!is_base_level && custom_tex_manager.SkipMipmaps()) { + return true; + } + + surface->is_custom = true; + + const auto upload = [this, level, surface, material]() -> bool { + if (!surface->IsCustom() && !surface->Swap(material)) { + LOG_ERROR(HW_GPU, "Custom compressed format {} unsupported by host GPU", + material->format); + return false; + } + surface->UploadCustom(material, level); + if (custom_tex_manager.SkipMipmaps()) { + runtime.GenerateMipmaps(*surface); + } + return true; + }; + return custom_tex_manager.Decode(material, std::move(upload)); +} + +u64 RasterizerCache::ComputeCustomHash(const SurfaceParams& load_info, std::span decoded, + std::span upload_data) { + MICROPROFILE_SCOPE(RasterizerCache_ComputeHash); + + if (custom_tex_manager.UseNewHash()) { + return Common::ComputeHash64(upload_data.data(), upload_data.size()); + } + return Common::ComputeHash64(decoded.data(), decoded.size()); +} + void RasterizerCache::DownloadSurface(const SurfaceRef& surface, SurfaceInterval interval) { + MICROPROFILE_SCOPE(RasterizerCache_DownloadSurface); + const SurfaceParams flush_info = surface->FromInterval(interval); const u32 flush_start = boost::icl::first(interval); const u32 flush_end = boost::icl::last_next(interval); diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index 9607a9657..3357d877a 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -4,7 +4,9 @@ #pragma once +#include #include +#include #include #include #include "video_core/rasterizer_cache/surface_base.h" @@ -28,6 +30,8 @@ enum class ScaleMatch { Ignore // accept every scaled res }; +class CustomTexManager; +struct CustomTexture; class RendererBase; class RasterizerCache : NonCopyable { @@ -62,10 +66,13 @@ public: }; public: - RasterizerCache(Memory::MemorySystem& memory, OpenGL::TextureRuntime& runtime, Pica::Regs& regs, - RendererBase& renderer); + RasterizerCache(Memory::MemorySystem& memory, CustomTexManager& custom_tex_manager, + OpenGL::TextureRuntime& runtime, Pica::Regs& regs, RendererBase& renderer); ~RasterizerCache(); + /// Notify the cache that a new frame has been queued + void TickFrame(); + /// Perform hardware accelerated texture copy according to the provided configuration bool AccelerateTextureCopy(const GPU::Regs::DisplayTransferConfig& config); @@ -126,6 +133,13 @@ private: /// Copies pixel data in interval from the guest VRAM to the host GPU surface void UploadSurface(const SurfaceRef& surface, SurfaceInterval interval); + /// Uploads a custom texture identified with hash to the target surface + bool UploadCustomSurface(const SurfaceRef& surface, const SurfaceParams& load_info, u64 hash); + + /// Returns the hash used to lookup the custom surface. + u64 ComputeCustomHash(const SurfaceParams& load_info, std::span decoded, + std::span upload_data); + /// Copies pixel data in interval from the host GPU surface to the guest VRAM void DownloadSurface(const SurfaceRef& surface, SurfaceInterval interval); @@ -158,6 +172,7 @@ private: private: Memory::MemorySystem& memory; + CustomTexManager& custom_tex_manager; OpenGL::TextureRuntime& runtime; Pica::Regs& regs; RendererBase& renderer; @@ -168,7 +183,9 @@ private: u32 resolution_scale_factor; RenderTargets render_targets; std::unordered_map texture_cube_cache; - bool use_filter{}; + bool use_filter; + bool dump_textures; + bool use_custom_textures; }; } // namespace VideoCore diff --git a/src/video_core/rasterizer_cache/surface_base.cpp b/src/video_core/rasterizer_cache/surface_base.cpp index 7d55ecec1..0826be1ca 100644 --- a/src/video_core/rasterizer_cache/surface_base.cpp +++ b/src/video_core/rasterizer_cache/surface_base.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include "common/alignment.h" +#include "video_core/custom_textures/material.h" #include "video_core/rasterizer_cache/surface_base.h" #include "video_core/texture/texture_decode.h" @@ -101,6 +102,10 @@ SurfaceInterval SurfaceBase::GetCopyableInterval(const SurfaceParams& params) co return result; } +bool SurfaceBase::HasNormalMap() const noexcept { + return material && material->Map(MapType::Normal) != nullptr; +} + ClearValue SurfaceBase::MakeClearValue(PAddr copy_addr, PixelFormat dst_format) { const SurfaceType dst_type = GetFormatType(dst_format); const std::array fill_buffer = MakeFillBuffer(copy_addr); diff --git a/src/video_core/rasterizer_cache/surface_base.h b/src/video_core/rasterizer_cache/surface_base.h index 05acff6c5..3deb989b5 100644 --- a/src/video_core/rasterizer_cache/surface_base.h +++ b/src/video_core/rasterizer_cache/surface_base.h @@ -11,6 +11,8 @@ namespace VideoCore { using SurfaceRegions = boost::icl::interval_set; +struct Material; + class SurfaceBase : public SurfaceParams { public: SurfaceBase(const SurfaceParams& params); @@ -28,10 +30,17 @@ public: /// Returns the clear value used to validate another surface from this fill surface ClearValue MakeClearValue(PAddr copy_addr, PixelFormat dst_format); + /// Returns true if the surface contains a custom material with a normal map. + bool HasNormalMap() const noexcept; + u64 ModificationTick() const noexcept { return modification_tick; } + bool IsCustom() const noexcept { + return is_custom && custom_format != CustomPixelFormat::Invalid; + } + bool IsRegionValid(SurfaceInterval interval) const { return (invalid_regions.find(interval) == invalid_regions.end()); } @@ -57,6 +66,8 @@ private: public: bool registered = false; + bool is_custom = false; + const Material* material = nullptr; SurfaceRegions invalid_regions; u32 fill_size = 0; std::array fill_data; diff --git a/src/video_core/rasterizer_cache/surface_params.cpp b/src/video_core/rasterizer_cache/surface_params.cpp index bbd3e38ce..7caae3c9e 100644 --- a/src/video_core/rasterizer_cache/surface_params.cpp +++ b/src/video_core/rasterizer_cache/surface_params.cpp @@ -215,12 +215,12 @@ u32 SurfaceParams::LevelOf(PAddr level_addr) const { return level; } -std::string SurfaceParams::DebugName(bool scaled) const noexcept { +std::string SurfaceParams::DebugName(bool scaled, bool custom) const noexcept { const u32 scaled_width = scaled ? GetScaledWidth() : width; const u32 scaled_height = scaled ? GetScaledHeight() : height; - return fmt::format("Surface: {}x{} {} {} levels from {:#x} to {:#x} ({})", scaled_width, + return fmt::format("Surface: {}x{} {} {} levels from {:#x} to {:#x} ({}{})", scaled_width, scaled_height, PixelFormatAsString(pixel_format), levels, addr, end, - scaled ? "scaled" : "unscaled"); + custom ? "custom," : "", scaled ? "scaled" : "unscaled"); } } // namespace VideoCore diff --git a/src/video_core/rasterizer_cache/surface_params.h b/src/video_core/rasterizer_cache/surface_params.h index 2787e440a..e71f4716b 100644 --- a/src/video_core/rasterizer_cache/surface_params.h +++ b/src/video_core/rasterizer_cache/surface_params.h @@ -4,6 +4,7 @@ #pragma once +#include "video_core/custom_textures/custom_format.h" #include "video_core/rasterizer_cache/utils.h" namespace VideoCore { @@ -46,7 +47,7 @@ public: u32 LevelOf(PAddr addr) const; /// Returns a string identifier of the params object - std::string DebugName(bool scaled) const noexcept; + std::string DebugName(bool scaled, bool custom = false) const noexcept; [[nodiscard]] SurfaceInterval GetInterval() const noexcept { return SurfaceInterval{addr, end}; @@ -101,6 +102,7 @@ public: bool is_tiled = false; TextureType texture_type = TextureType::Texture2D; PixelFormat pixel_format = PixelFormat::Invalid; + CustomPixelFormat custom_format = CustomPixelFormat::Invalid; SurfaceType type = SurfaceType::Invalid; std::array mipmap_offsets{}; diff --git a/src/video_core/renderer_opengl/gl_blit_helper.cpp b/src/video_core/renderer_opengl/gl_blit_helper.cpp index e41dda81d..b8223aa0a 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.cpp +++ b/src/video_core/renderer_opengl/gl_blit_helper.cpp @@ -13,7 +13,6 @@ #include "video_core/host_shaders/texture_filtering/nearest_neighbor_frag.h" #include "video_core/host_shaders/texture_filtering/refine_frag.h" #include "video_core/host_shaders/texture_filtering/scale_force_frag.h" -#include "video_core/host_shaders/texture_filtering/tex_coord_vert.h" #include "video_core/host_shaders/texture_filtering/x_gradient_frag.h" #include "video_core/host_shaders/texture_filtering/xbrz_freescale_frag.h" #include "video_core/host_shaders/texture_filtering/y_gradient_frag.h" @@ -81,7 +80,7 @@ bool BlitHelper::Filter(Surface& surface, const VideoCore::TextureBlit& blit) { } const OpenGLState prev_state = OpenGLState::GetCurState(); - state.texture_units[0].texture_2d = surface.Handle(false); + state.texture_units[0].texture_2d = surface.Handle(0); const auto filter{Settings::values.texture_filter.GetValue()}; switch (filter) { @@ -135,7 +134,7 @@ void BlitHelper::FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& b auto LUMAD = setup_temp_tex(GL_R16F, GL_RED, temp_rect.GetWidth(), temp_rect.GetHeight()); // Copy to SRC - glCopyImageSubData(surface.Handle(false), GL_TEXTURE_2D, 0, blit.src_rect.left, + glCopyImageSubData(surface.Handle(0), GL_TEXTURE_2D, 0, blit.src_rect.left, blit.src_rect.bottom, 0, SRC.tex.handle, GL_TEXTURE_2D, 0, 0, 0, 0, src_width, src_height, 1); @@ -161,47 +160,42 @@ void BlitHelper::FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& b } void BlitHelper::FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit) { - SetParams(bicubic_program, surface.width, surface.height, blit.src_rect); + SetParams(bicubic_program, surface.Extent(), blit.src_rect); Draw(bicubic_program, surface.Handle(), filter_fbo.handle, blit.dst_level, blit.dst_rect); } void BlitHelper::FilterNearest(Surface& surface, const VideoCore::TextureBlit& blit) { - state.texture_units[2].texture_2d = surface.Handle(false); - SetParams(nearest_program, surface.width, surface.height, blit.src_rect); + state.texture_units[2].texture_2d = surface.Handle(0); + SetParams(nearest_program, surface.Extent(), blit.src_rect); Draw(nearest_program, surface.Handle(), filter_fbo.handle, blit.dst_level, blit.dst_rect); } void BlitHelper::FilterScaleForce(Surface& surface, const VideoCore::TextureBlit& blit) { - SetParams(scale_force_program, surface.width, surface.height, blit.src_rect); + SetParams(scale_force_program, surface.Extent(), blit.src_rect); Draw(scale_force_program, surface.Handle(), filter_fbo.handle, blit.dst_level, blit.dst_rect); } void BlitHelper::FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit) { glProgramUniform1f(xbrz_program.handle, 2, static_cast(surface.res_scale)); - SetParams(xbrz_program, surface.width, surface.height, blit.src_rect); + SetParams(xbrz_program, surface.Extent(), blit.src_rect); Draw(xbrz_program, surface.Handle(), filter_fbo.handle, blit.dst_level, blit.dst_rect); } -void BlitHelper::SetParams(OGLProgram& program, u32 src_width, u32 src_height, +void BlitHelper::SetParams(OGLProgram& program, const VideoCore::Extent& src_extent, Common::Rectangle src_rect) { glProgramUniform2f( program.handle, 0, - static_cast(src_rect.right - src_rect.left) / static_cast(src_width), - static_cast(src_rect.top - src_rect.bottom) / static_cast(src_height)); + static_cast(src_rect.right - src_rect.left) / static_cast(src_extent.width), + static_cast(src_rect.top - src_rect.bottom) / static_cast(src_extent.height)); glProgramUniform2f(program.handle, 1, - static_cast(src_rect.left) / static_cast(src_width), - static_cast(src_rect.bottom) / static_cast(src_height)); + static_cast(src_rect.left) / static_cast(src_extent.width), + static_cast(src_rect.bottom) / static_cast(src_extent.height)); } void BlitHelper::Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 dst_level, Common::Rectangle dst_rect) { state.draw.draw_framebuffer = dst_fbo; state.draw.shader_program = program.handle; - state.scissor.enabled = true; - state.scissor.x = dst_rect.left; - state.scissor.y = dst_rect.bottom; - state.scissor.width = dst_rect.GetWidth(); - state.scissor.height = dst_rect.GetHeight(); state.viewport.x = dst_rect.left; state.viewport.y = dst_rect.bottom; state.viewport.width = dst_rect.GetWidth(); @@ -212,7 +206,6 @@ void BlitHelper::Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 d dst_level); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } diff --git a/src/video_core/renderer_opengl/gl_blit_helper.h b/src/video_core/renderer_opengl/gl_blit_helper.h index 842c5ff9b..3c156b502 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.h +++ b/src/video_core/renderer_opengl/gl_blit_helper.h @@ -9,8 +9,9 @@ #include "video_core/renderer_opengl/gl_state.h" namespace VideoCore { +struct Extent; struct TextureBlit; -} +} // namespace VideoCore namespace OpenGL { @@ -35,7 +36,7 @@ private: void FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit); - void SetParams(OGLProgram& program, u32 src_width, u32 src_height, + void SetParams(OGLProgram& program, const VideoCore::Extent& src_extent, Common::Rectangle src_rect); void Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 dst_level, diff --git a/src/video_core/renderer_opengl/gl_driver.cpp b/src/video_core/renderer_opengl/gl_driver.cpp index cb2f05732..08f6b9ae9 100644 --- a/src/video_core/renderer_opengl/gl_driver.cpp +++ b/src/video_core/renderer_opengl/gl_driver.cpp @@ -6,6 +6,7 @@ #include "common/assert.h" #include "common/settings.h" #include "core/telemetry_session.h" +#include "video_core/custom_textures/custom_format.h" #include "video_core/renderer_opengl/gl_driver.h" namespace OpenGL { @@ -103,6 +104,25 @@ bool Driver::HasDebugTool() { return false; } +bool Driver::IsCustomFormatSupported(VideoCore::CustomPixelFormat format) const { + switch (format) { + case VideoCore::CustomPixelFormat::RGBA8: + return true; + case VideoCore::CustomPixelFormat::BC1: + case VideoCore::CustomPixelFormat::BC3: + case VideoCore::CustomPixelFormat::BC5: + return ext_texture_compression_s3tc; + case VideoCore::CustomPixelFormat::BC7: + return arb_texture_compression_bptc; + case VideoCore::CustomPixelFormat::ASTC4: + case VideoCore::CustomPixelFormat::ASTC6: + case VideoCore::CustomPixelFormat::ASTC8: + return is_gles; + default: + return false; + } +} + void Driver::ReportDriverInfo() { // Report the context version and the vendor string gl_version = std::string_view{reinterpret_cast(glGetString(GL_VERSION))}; @@ -145,7 +165,9 @@ void Driver::CheckExtensionSupport() { arb_buffer_storage = GLAD_GL_ARB_buffer_storage; arb_clear_texture = GLAD_GL_ARB_clear_texture; arb_get_texture_sub_image = GLAD_GL_ARB_get_texture_sub_image; + arb_texture_compression_bptc = GLAD_GL_ARB_texture_compression_bptc; ext_clip_cull_distance = GLAD_GL_EXT_clip_cull_distance; + ext_texture_compression_s3tc = GLAD_GL_EXT_texture_compression_s3tc; is_suitable = GLAD_GL_VERSION_4_3 || GLAD_GL_ES_VERSION_3_1; } diff --git a/src/video_core/renderer_opengl/gl_driver.h b/src/video_core/renderer_opengl/gl_driver.h index b155a890a..40044b181 100644 --- a/src/video_core/renderer_opengl/gl_driver.h +++ b/src/video_core/renderer_opengl/gl_driver.h @@ -11,6 +11,10 @@ namespace Core { class TelemetrySession; } +namespace VideoCore { +enum class CustomPixelFormat : u32; +} + namespace OpenGL { enum class Vendor { @@ -51,6 +55,9 @@ public: /// Returns true if any debug tool is attached bool HasDebugTool(); + /// Returns true if the driver supports the provided custom format + bool IsCustomFormatSupported(VideoCore::CustomPixelFormat format) const; + /// Returns the vendor of the currently selected physical device Vendor GetVendor() const { return vendor; @@ -114,6 +121,8 @@ private: bool arb_clear_texture{}; bool arb_get_texture_sub_image{}; bool ext_clip_cull_distance{}; + bool ext_texture_compression_s3tc{}; + bool arb_texture_compression_bptc{}; std::string_view gl_version{}; std::string_view gpu_vendor{}; diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index e20b9b8c1..55cc9f117 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -73,11 +73,13 @@ GLenum MakeAttributeType(Pica::PipelineRegs::VertexAttributeFormat format) { } // Anonymous namespace -RasterizerOpenGL::RasterizerOpenGL(Memory::MemorySystem& memory, VideoCore::RendererBase& renderer, - Driver& driver_) +RasterizerOpenGL::RasterizerOpenGL(Memory::MemorySystem& memory, + VideoCore::CustomTexManager& custom_tex_manager, + VideoCore::RendererBase& renderer, Driver& driver_) : VideoCore::RasterizerAccelerated{memory}, driver{driver_}, runtime{driver, renderer}, - res_cache{memory, runtime, regs, renderer}, texture_buffer_size{TextureBufferSize()}, - vertex_buffer{driver, GL_ARRAY_BUFFER, VERTEX_BUFFER_SIZE}, + res_cache{memory, custom_tex_manager, runtime, regs, renderer}, + texture_buffer_size{TextureBufferSize()}, vertex_buffer{driver, GL_ARRAY_BUFFER, + VERTEX_BUFFER_SIZE}, uniform_buffer{driver, GL_UNIFORM_BUFFER, UNIFORM_BUFFER_SIZE}, index_buffer{driver, GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE}, texture_buffer{driver, GL_TEXTURE_BUFFER, texture_buffer_size}, texture_lf_buffer{ @@ -183,6 +185,10 @@ RasterizerOpenGL::RasterizerOpenGL(Memory::MemorySystem& memory, VideoCore::Rend RasterizerOpenGL::~RasterizerOpenGL() = default; +void RasterizerOpenGL::TickFrame() { + res_cache.TickFrame(); +} + void RasterizerOpenGL::LoadDiskResources(const std::atomic_bool& stop_loading, const VideoCore::DiskResourceLoadCallback& callback) { shader_program_manager->LoadDiskCache(stop_loading, callback); @@ -420,7 +426,6 @@ bool RasterizerOpenGL::Draw(bool accelerate, bool is_indexed) { state.scissor.y = draw_rect.bottom; state.scissor.width = draw_rect.GetWidth(); state.scissor.height = draw_rect.GetHeight(); - state.Apply(); const u32 res_scale = framebuffer.ResolutionScale(); if (uniform_block_data.data.framebuffer_scale != res_scale) { @@ -444,10 +449,11 @@ bool RasterizerOpenGL::Draw(bool accelerate, bool is_indexed) { // Sync and bind the texture surfaces SyncTextureUnits(framebuffer); + state.Apply(); // Sync and bind the shader if (shader_dirty) { - SetShader(); + shader_program_manager->UseFragmentShader(regs, use_custom_normal); shader_dirty = false; } @@ -546,6 +552,7 @@ void RasterizerOpenGL::SyncTextureUnits(const Framebuffer& framebuffer) { // on the male character's face, which in the OpenGL default appear black. state.texture_units[texture_index].texture_2d = default_texture; } else if (!IsFeedbackLoop(texture_index, framebuffer, *surface)) { + BindMaterial(texture_index, *surface); state.texture_units[texture_index].texture_2d = surface->Handle(); } } @@ -589,6 +596,33 @@ void RasterizerOpenGL::BindTextureCube(const Pica::TexturingRegs::FullTextureCon state.texture_units[0].texture_2d = 0; } +void RasterizerOpenGL::BindMaterial(u32 texture_index, Surface& surface) { + if (!surface.IsCustom() || texture_index != 0) { + return; + } + + const auto bind_texture = [&](const TextureUnits::TextureUnit& unit, GLuint texture, + GLuint sampler) { + glActiveTexture(unit.Enum()); + glBindTexture(GL_TEXTURE_2D, texture); + glBindSampler(unit.id, sampler); + }; + + const GLuint sampler = texture_samplers[texture_index].sampler.handle; + if (surface.HasNormalMap()) { + if (regs.lighting.disable) { + LOG_WARNING(Render_OpenGL, "Custom normal map used but scene has no light enabled"); + } + bind_texture(TextureUnits::TextureNormalMap, surface.Handle(2), sampler); + use_custom_normal = true; + } else { + if (use_custom_normal) { + bind_texture(TextureUnits::TextureNormalMap, 0, 0); + } + use_custom_normal = false; + } +} + bool RasterizerOpenGL::IsFeedbackLoop(u32 texture_index, const Framebuffer& framebuffer, Surface& surface) { const GLuint color_attachment = framebuffer.Attachment(SurfaceType::Color); @@ -622,7 +656,6 @@ void RasterizerOpenGL::UnbindSpecial() { state.image_shadow_texture_pz = 0; state.image_shadow_texture_nz = 0; state.image_shadow_buffer = 0; - state.Apply(); } void RasterizerOpenGL::NotifyFixedFunctionPicaRegisterChanged(u32 id) { @@ -823,10 +856,6 @@ void RasterizerOpenGL::SamplerInfo::SyncWithConfig( } } -void RasterizerOpenGL::SetShader() { - shader_program_manager->UseFragmentShader(Pica::g_state.regs); -} - void RasterizerOpenGL::SyncClipEnabled() { state.clip_distance[1] = Pica::g_state.regs.rasterizer.clip_enable != 0; } diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h index f91c68f85..73a25cdd5 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.h +++ b/src/video_core/renderer_opengl/gl_rasterizer.h @@ -18,6 +18,10 @@ namespace VideoCore { class RendererBase; } +namespace VideoCore { +class CustomTexManager; +} + namespace OpenGL { class Driver; @@ -25,10 +29,12 @@ class ShaderProgramManager; class RasterizerOpenGL : public VideoCore::RasterizerAccelerated { public: - explicit RasterizerOpenGL(Memory::MemorySystem& memory, VideoCore::RendererBase& renderer, - Driver& driver); + explicit RasterizerOpenGL(Memory::MemorySystem& memory, + VideoCore::CustomTexManager& custom_tex_manager, + VideoCore::RendererBase& renderer, Driver& driver); ~RasterizerOpenGL() override; + void TickFrame(); void LoadDiskResources(const std::atomic_bool& stop_loading, const VideoCore::DiskResourceLoadCallback& callback) override; @@ -74,9 +80,6 @@ private: /// Syncs the clip enabled status to match the PICA register void SyncClipEnabled(); - /// Sets the OpenGL shader in accordance with the current PICA register state - void SetShader(); - /// Syncs the cull mode to match the PICA register void SyncCullMode(); @@ -126,6 +129,9 @@ private: /// Unbinds all special texture unit 0 texture configurations void UnbindSpecial(); + /// Binds the custom material referenced by surface if it exists. + void BindMaterial(u32 texture_index, Surface& surface); + /// Upload the uniform blocks to the uniform buffer object void UploadUniforms(bool accelerate_draw); @@ -174,6 +180,7 @@ private: OGLTexture texture_buffer_lut_lf; OGLTexture texture_buffer_lut_rg; OGLTexture texture_buffer_lut_rgba; + bool use_custom_normal{}; }; } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_shader_gen.cpp b/src/video_core/renderer_opengl/gl_shader_gen.cpp index 7686980de..577fd3494 100644 --- a/src/video_core/renderer_opengl/gl_shader_gen.cpp +++ b/src/video_core/renderer_opengl/gl_shader_gen.cpp @@ -59,7 +59,7 @@ out gl_PerVertex { return out; } -PicaFSConfig PicaFSConfig::BuildFromRegs(const Pica::Regs& regs) { +PicaFSConfig PicaFSConfig::BuildFromRegs(const Pica::Regs& regs, bool use_normal) { PicaFSConfig res{}; auto& state = res.state; @@ -204,6 +204,8 @@ PicaFSConfig PicaFSConfig::BuildFromRegs(const Pica::Regs& regs) { state.shadow_texture_orthographic = regs.texturing.shadow.orthographic != 0; + state.use_custom_normal_map = use_normal; + return res; } @@ -297,6 +299,8 @@ static std::string SampleTexture(const PicaFSConfig& config, unsigned texture_un LOG_DEBUG(Render_OpenGL, "Using Texture3 without enabling it"); return "vec4(0.0)"; } + case 4: + return "texture(tex_normal, texcoord0)"; default: UNREACHABLE(); return ""; @@ -642,7 +646,12 @@ static void WriteLighting(std::string& out, const PicaFSConfig& config) { const auto Perturbation = [&] { return fmt::format("2.0 * ({}).rgb - 1.0", SampleTexture(config, lighting.bump_selector)); }; - if (lighting.bump_mode == LightingRegs::LightingBumpMode::NormalMap) { + if (config.state.use_custom_normal_map) { + const std::string normal_texel = + fmt::format("2.0 * ({}).rgb - 1.0", SampleTexture(config, 4)); + out += fmt::format("vec3 surface_normal = {};\n", normal_texel); + out += "vec3 surface_tangent = vec3(1.0, 0.0, 0.0);\n"; + } else if (lighting.bump_mode == LightingRegs::LightingBumpMode::NormalMap) { // Bump mapping is enabled using a normal map out += fmt::format("vec3 surface_normal = {};\n", Perturbation()); @@ -1207,6 +1216,7 @@ out vec4 color; uniform sampler2D tex0; uniform sampler2D tex1; uniform sampler2D tex2; +uniform sampler2D tex_normal; //< Used for custom normal maps uniform samplerCube tex_cube; uniform samplerBuffer texture_buffer_lut_lf; uniform samplerBuffer texture_buffer_lut_rg; diff --git a/src/video_core/renderer_opengl/gl_shader_gen.h b/src/video_core/renderer_opengl/gl_shader_gen.h index 027573216..557e58407 100644 --- a/src/video_core/renderer_opengl/gl_shader_gen.h +++ b/src/video_core/renderer_opengl/gl_shader_gen.h @@ -117,6 +117,7 @@ struct PicaFSConfigState { bool shadow_rendering; bool shadow_texture_orthographic; + bool use_custom_normal_map; }; /** @@ -130,7 +131,7 @@ struct PicaFSConfigState { struct PicaFSConfig : Common::HashableStruct { /// Construct a PicaFSConfig with the given Pica register configuration. - static PicaFSConfig BuildFromRegs(const Pica::Regs& regs); + static PicaFSConfig BuildFromRegs(const Pica::Regs& regs, bool use_normal = false); bool TevStageUpdatesCombinerBufferColor(unsigned stage_index) const { return (stage_index < 4) && (state.combiner_buffer_input & (1 << stage_index)); diff --git a/src/video_core/renderer_opengl/gl_shader_manager.cpp b/src/video_core/renderer_opengl/gl_shader_manager.cpp index ab1c20b1d..0be733e95 100644 --- a/src/video_core/renderer_opengl/gl_shader_manager.cpp +++ b/src/video_core/renderer_opengl/gl_shader_manager.cpp @@ -133,6 +133,7 @@ static void SetShaderSamplerBindings(GLuint shader) { SetShaderSamplerBinding(shader, "tex1", TextureUnits::PicaTexture(1)); SetShaderSamplerBinding(shader, "tex2", TextureUnits::PicaTexture(2)); SetShaderSamplerBinding(shader, "tex_cube", TextureUnits::TextureCube); + SetShaderSamplerBinding(shader, "tex_normal", TextureUnits::TextureNormalMap); // Set the texture samplers to correspond to different lookup table texture units SetShaderSamplerBinding(shader, "texture_buffer_lut_lf", TextureUnits::TextureBufferLUT_LF); @@ -415,8 +416,8 @@ void ShaderProgramManager::UseTrivialGeometryShader() { impl->current.gs_hash = 0; } -void ShaderProgramManager::UseFragmentShader(const Pica::Regs& regs) { - PicaFSConfig config = PicaFSConfig::BuildFromRegs(regs); +void ShaderProgramManager::UseFragmentShader(const Pica::Regs& regs, bool use_normal) { + PicaFSConfig config = PicaFSConfig::BuildFromRegs(regs, use_normal); auto [handle, result] = impl->fragment_shaders.Get(config); impl->current.fs = handle; impl->current.fs_hash = config.Hash(); diff --git a/src/video_core/renderer_opengl/gl_shader_manager.h b/src/video_core/renderer_opengl/gl_shader_manager.h index d0b59754b..4b7104be7 100644 --- a/src/video_core/renderer_opengl/gl_shader_manager.h +++ b/src/video_core/renderer_opengl/gl_shader_manager.h @@ -41,7 +41,7 @@ public: void UseTrivialGeometryShader(); - void UseFragmentShader(const Pica::Regs& config); + void UseFragmentShader(const Pica::Regs& config, bool use_normal); void ApplyTo(OpenGLState& state); diff --git a/src/video_core/renderer_opengl/gl_state.h b/src/video_core/renderer_opengl/gl_state.h index 418959749..2a5ef5edb 100644 --- a/src/video_core/renderer_opengl/gl_state.h +++ b/src/video_core/renderer_opengl/gl_state.h @@ -26,6 +26,7 @@ constexpr TextureUnit TextureCube{6}; constexpr TextureUnit TextureBufferLUT_LF{3}; constexpr TextureUnit TextureBufferLUT_RG{4}; constexpr TextureUnit TextureBufferLUT_RGBA{5}; +constexpr TextureUnit TextureNormalMap{7}; } // namespace TextureUnits diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index 3ab3b475e..b461575a2 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -3,7 +3,7 @@ // Refer to the license.txt file included. #include "common/scope_exit.h" -#include "common/settings.h" +#include "video_core/custom_textures/material.h" #include "video_core/regs.h" #include "video_core/renderer_base.h" #include "video_core/renderer_opengl/gl_driver.h" @@ -14,8 +14,10 @@ namespace OpenGL { namespace { +using VideoCore::MapType; using VideoCore::PixelFormat; using VideoCore::SurfaceType; +using VideoCore::TextureType; constexpr FormatTuple DEFAULT_TUPLE = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE}; @@ -42,6 +44,17 @@ static constexpr std::array COLOR_TUPLES_OES = {{ {GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4}, // RGBA4 }}; +static constexpr std::array CUSTOM_TUPLES = {{ + DEFAULT_TUPLE, + {GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL_UNSIGNED_BYTE}, + {GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, GL_UNSIGNED_BYTE}, + {GL_COMPRESSED_RG_RGTC2, GL_COMPRESSED_RG_RGTC2, GL_UNSIGNED_BYTE}, + {GL_COMPRESSED_RGBA_BPTC_UNORM_ARB, GL_COMPRESSED_RGBA_BPTC_UNORM_ARB, GL_UNSIGNED_BYTE}, + {GL_COMPRESSED_RGBA_ASTC_4x4, GL_COMPRESSED_RGBA_ASTC_4x4, GL_UNSIGNED_BYTE}, + {GL_COMPRESSED_RGBA_ASTC_6x6, GL_COMPRESSED_RGBA_ASTC_6x6, GL_UNSIGNED_BYTE}, + {GL_COMPRESSED_RGBA_ASTC_8x6, GL_COMPRESSED_RGBA_ASTC_8x6, GL_UNSIGNED_BYTE}, +}}; + struct FramebufferInfo { GLuint color; GLuint depth; @@ -109,17 +122,23 @@ TextureRuntime::TextureRuntime(const Driver& driver_, VideoCore::RendererBase& r read_fbos[i].Create(); } - auto Register = [this](PixelFormat dest, std::unique_ptr&& obj) { + auto add_reinterpreter = [this](PixelFormat dest, + std::unique_ptr&& obj) { const u32 dst_index = static_cast(dest); return reinterpreters[dst_index].push_back(std::move(obj)); }; - Register(PixelFormat::RGBA8, std::make_unique()); - Register(PixelFormat::RGB5A1, std::make_unique()); + add_reinterpreter(PixelFormat::RGBA8, std::make_unique()); + add_reinterpreter(PixelFormat::RGB5A1, std::make_unique()); } TextureRuntime::~TextureRuntime() = default; +void TextureRuntime::Reset() { + alloc_cache.clear(); + framebuffer_cache.clear(); +} + bool TextureRuntime::NeedsConversion(VideoCore::PixelFormat pixel_format) const { const bool should_convert = pixel_format == PixelFormat::RGBA8 || // Needs byteswap pixel_format == PixelFormat::RGB8; // Is converted to RGBA8 @@ -153,51 +172,64 @@ const FormatTuple& TextureRuntime::GetFormatTuple(PixelFormat pixel_format) cons return DEFAULT_TUPLE; } -void TextureRuntime::Recycle(const HostTextureTag tag, Allocation&& alloc) { - recycler.emplace(tag, std::move(alloc)); +const FormatTuple& TextureRuntime::GetFormatTuple(VideoCore::CustomPixelFormat pixel_format) { + const std::size_t format_index = static_cast(pixel_format); + return CUSTOM_TUPLES[format_index]; } -Allocation TextureRuntime::Allocate(const VideoCore::SurfaceParams& params) { - const u32 width = params.width; - const u32 height = params.height; - const u32 levels = params.levels; +void TextureRuntime::Recycle(const HostTextureTag tag, Allocation&& alloc) { + alloc_cache.emplace(tag, std::move(alloc)); +} + +Allocation TextureRuntime::Allocate(const VideoCore::SurfaceParams& params, + const VideoCore::Material* material) { const GLenum target = params.texture_type == VideoCore::TextureType::CubeMap ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D; - const auto& tuple = GetFormatTuple(params.pixel_format); - + const bool is_custom = material != nullptr; + const bool has_normal = material && material->Map(MapType::Normal); + const auto& tuple = + is_custom ? GetFormatTuple(params.custom_format) : GetFormatTuple(params.pixel_format); const HostTextureTag key = { + .width = params.width, + .height = params.height, + .levels = params.levels, + .res_scale = params.res_scale, .tuple = tuple, .type = params.texture_type, - .width = width, - .height = height, - .levels = levels, - .res_scale = params.res_scale, + .is_custom = is_custom, + .has_normal = has_normal, }; - if (auto it = recycler.find(key); it != recycler.end()) { - Allocation alloc = std::move(it->second); - ASSERT(alloc.res_scale == params.res_scale); - recycler.erase(it); + if (auto it = alloc_cache.find(key); it != alloc_cache.end()) { + auto alloc{std::move(it->second)}; + alloc_cache.erase(it); return alloc; } const GLuint old_tex = OpenGLState::GetCurState().texture_units[0].texture_2d; glActiveTexture(GL_TEXTURE0); - std::array textures{}; - std::array handles{}; + std::array textures{}; + std::array handles{}; - textures[0] = MakeHandle(target, width, height, levels, tuple, params.DebugName(false)); + textures[0] = MakeHandle(target, params.width, params.height, params.levels, tuple, + params.DebugName(false)); handles.fill(textures[0].handle); if (params.res_scale != 1) { - const u32 scaled_width = params.GetScaledWidth(); - const u32 scaled_height = params.GetScaledHeight(); - textures[1] = - MakeHandle(target, scaled_width, scaled_height, levels, tuple, params.DebugName(true)); + const u32 scaled_width = is_custom ? params.width : params.GetScaledWidth(); + const u32 scaled_height = is_custom ? params.height : params.GetScaledHeight(); + const auto& scaled_tuple = is_custom ? GetFormatTuple(PixelFormat::RGBA8) : tuple; + textures[1] = MakeHandle(target, scaled_width, scaled_height, params.levels, scaled_tuple, + params.DebugName(true, is_custom)); handles[1] = textures[1].handle; } + if (has_normal) { + textures[2] = MakeHandle(target, params.width, params.height, params.levels, tuple, + params.DebugName(true, is_custom)); + handles[2] = textures[2].handle; + } glBindTexture(GL_TEXTURE_2D, old_tex); @@ -311,15 +343,20 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, return true; } -void TextureRuntime::GenerateMipmaps(Surface& surface, u32 max_level) { +void TextureRuntime::GenerateMipmaps(Surface& surface) { OpenGLState state = OpenGLState::GetCurState(); - state.texture_units[0].texture_2d = surface.Handle(); - state.Apply(); - glActiveTexture(GL_TEXTURE0); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, max_level); + const auto generate = [&](u32 index) { + state.texture_units[0].texture_2d = surface.Handle(index); + state.Apply(); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, surface.levels - 1); + glGenerateMipmap(GL_TEXTURE_2D); + }; - glGenerateMipmap(GL_TEXTURE_2D); + generate(1); + if (surface.HasNormalMap()) { + generate(2); + } } const ReinterpreterList& TextureRuntime::GetPossibleReinterpretations( @@ -340,21 +377,11 @@ Surface::~Surface() { if (pixel_format == PixelFormat::Invalid || !alloc) { return; } - - const HostTextureTag tag = { - .tuple = alloc.tuple, - .type = texture_type, - .width = alloc.width, - .height = alloc.height, - .levels = alloc.levels, - .res_scale = alloc.res_scale, - }; - runtime->Recycle(tag, std::move(alloc)); + runtime->Recycle(MakeTag(), std::move(alloc)); } void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging) { - // Ensure no bad interactions with GL_UNPACK_ALIGNMENT ASSERT(stride * GetFormatBytesPerPixel(pixel_format) % 4 == 0); const GLuint old_tex = OpenGLState::GetCurState().texture_units[0].texture_2d; @@ -362,12 +389,10 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const u32 unscaled_height = upload.texture_rect.GetHeight(); glPixelStorei(GL_UNPACK_ROW_LENGTH, unscaled_width); - // Bind the unscaled texture glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, Handle(false)); + glBindTexture(GL_TEXTURE_2D, Handle(0)); - // Upload the requested rectangle of pixels - const auto& tuple = runtime->GetFormatTuple(pixel_format); + const auto& tuple = alloc.tuple; glTexSubImage2D(GL_TEXTURE_2D, upload.texture_level, upload.texture_rect.left, upload.texture_rect.bottom, unscaled_width, unscaled_height, tuple.format, tuple.type, staging.mapped.data()); @@ -381,17 +406,61 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .src_rect = upload.texture_rect, .dst_rect = upload.texture_rect * res_scale, }; - - // If texture filtering is enabled attempt to upscale with that, otherwise fallback - // to normal blit. if (res_scale != 1 && !runtime->blit_helper.Filter(*this, blit)) { BlitScale(blit, true); } } +void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { + const GLuint old_tex = OpenGLState::GetCurState().texture_units[0].texture_2d; + const auto& tuple = alloc.tuple; + const u32 width = material->width; + const u32 height = material->height; + const auto color = material->textures[0]; + const Common::Rectangle filter_rect{0U, height, width, 0U}; + + glActiveTexture(GL_TEXTURE0); + glPixelStorei(GL_UNPACK_ROW_LENGTH, width); + + glBindTexture(GL_TEXTURE_2D, Handle(0)); + if (VideoCore::IsCustomFormatCompressed(custom_format)) { + const GLsizei image_size = static_cast(color->data.size()); + glCompressedTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, width, height, tuple.format, + image_size, color->data.data()); + } else { + glTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, width, height, tuple.format, tuple.type, + color->data.data()); + } + + const VideoCore::TextureBlit blit = { + .src_rect = filter_rect, + .dst_rect = filter_rect, + }; + if (res_scale != 1 && !runtime->blit_helper.Filter(*this, blit)) { + BlitScale(blit, true); + } + for (u32 i = 1; i < VideoCore::MAX_MAPS; i++) { + const auto texture = material->textures[i]; + if (!texture) { + continue; + } + glBindTexture(GL_TEXTURE_2D, Handle(i + 1)); + if (VideoCore::IsCustomFormatCompressed(custom_format)) { + const GLsizei image_size = static_cast(texture->data.size()); + glCompressedTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, width, height, tuple.format, + image_size, texture->data.data()); + } else { + glTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, width, height, tuple.format, tuple.type, + texture->data.data()); + } + } + + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glBindTexture(GL_TEXTURE_2D, old_tex); +} + void Surface::Download(const VideoCore::BufferTextureCopy& download, const VideoCore::StagingData& staging) { - // Ensure no bad interactions with GL_PACK_ALIGNMENT ASSERT(stride * GetFormatBytesPerPixel(pixel_format) % 4 == 0); const u32 unscaled_width = download.texture_rect.GetWidth(); @@ -414,11 +483,9 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, return; } - const u32 fbo_index = FboIndex(type); - OpenGLState state = OpenGLState::GetCurState(); state.scissor.enabled = false; - state.draw.read_framebuffer = runtime->read_fbos[fbo_index].handle; + state.draw.read_framebuffer = runtime->read_fbos[FboIndex(type)].handle; state.Apply(); Attach(GL_READ_FRAMEBUFFER, download.texture_level, 0, false); @@ -428,14 +495,11 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, glReadPixels(download.texture_rect.left, download.texture_rect.bottom, unscaled_width, unscaled_height, tuple.format, tuple.type, staging.mapped.data()); - // Restore previous state glPixelStorei(GL_PACK_ROW_LENGTH, 0); } bool Surface::DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download, const VideoCore::StagingData& staging) { - // If a partial download is requested and ARB_get_texture_sub_image (core in 4.5) - // is not available we cannot proceed further. const bool is_full_download = download.texture_rect == GetRect(); const bool has_sub_image = driver->HasArbGetTextureSubImage(); if (driver->IsOpenGLES() || (!is_full_download && !has_sub_image)) { @@ -452,7 +516,7 @@ bool Surface::DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download, // Prefer glGetTextureSubImage in most cases since it's the fastest and most convenient option if (has_sub_image) { const GLsizei buf_size = static_cast(staging.mapped.size()); - glGetTextureSubImage(Handle(false), download.texture_level, download.texture_rect.left, + glGetTextureSubImage(Handle(0), download.texture_level, download.texture_rect.left, download.texture_rect.bottom, 0, download.texture_rect.GetWidth(), download.texture_rect.GetHeight(), 1, tuple.format, tuple.type, buf_size, staging.mapped.data()); @@ -461,7 +525,7 @@ bool Surface::DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download, // This should only trigger for full texture downloads in oldish intel drivers // that only support up to 4.3 - glBindTexture(GL_TEXTURE_2D, Handle(false)); + glBindTexture(GL_TEXTURE_2D, Handle(0)); glGetTexImage(GL_TEXTURE_2D, download.texture_level, tuple.format, tuple.type, staging.mapped.data()); glBindTexture(GL_TEXTURE_2D, old_tex); @@ -470,20 +534,20 @@ bool Surface::DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download, } void Surface::Attach(GLenum target, u32 level, u32 layer, bool scaled) { - const GLuint handle = Handle(scaled); - const GLenum textarget = texture_type == VideoCore::TextureType::CubeMap + const GLuint handle = Handle(static_cast(scaled)); + const GLenum textarget = texture_type == TextureType::CubeMap ? GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer : GL_TEXTURE_2D; switch (type) { - case VideoCore::SurfaceType::Color: - case VideoCore::SurfaceType::Texture: + case SurfaceType::Color: + case SurfaceType::Texture: glFramebufferTexture2D(target, GL_COLOR_ATTACHMENT0, textarget, handle, level); break; - case VideoCore::SurfaceType::Depth: + case SurfaceType::Depth: glFramebufferTexture2D(target, GL_DEPTH_ATTACHMENT, textarget, handle, level); break; - case VideoCore::SurfaceType::DepthStencil: + case SurfaceType::DepthStencil: glFramebufferTexture2D(target, GL_DEPTH_STENCIL_ATTACHMENT, textarget, handle, level); break; default: @@ -491,6 +555,30 @@ void Surface::Attach(GLenum target, u32 level, u32 layer, bool scaled) { } } +bool Surface::Swap(const VideoCore::Material* mat) { + const VideoCore::CustomPixelFormat format{mat->format}; + if (!driver->IsCustomFormatSupported(format)) { + return false; + } + runtime->Recycle(MakeTag(), std::move(alloc)); + + SurfaceParams params = *this; + params.width = mat->width; + params.height = mat->height; + params.custom_format = mat->format; + alloc = runtime->Allocate(params, mat); + + LOG_DEBUG(Render_OpenGL, "Swapped {}x{} {} surface at address {:#x} to {}x{} {}", + GetScaledWidth(), GetScaledHeight(), VideoCore::PixelFormatAsString(pixel_format), + addr, width, height, VideoCore::CustomPixelFormatAsString(format)); + + is_custom = true; + custom_format = format; + material = mat; + + return true; +} + u32 Surface::GetInternalBytesPerPixel() const { // RGB8 is converted to RGBA8 on OpenGL ES since it doesn't support BGR8 if (driver->IsOpenGLES() && pixel_format == VideoCore::PixelFormat::RGB8) { @@ -518,6 +606,19 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { blit.dst_rect.right, blit.dst_rect.top, buffer_mask, filter); } +HostTextureTag Surface::MakeTag() const noexcept { + return HostTextureTag{ + .width = alloc.width, + .height = alloc.height, + .levels = alloc.levels, + .res_scale = alloc.res_scale, + .tuple = alloc.tuple, + .type = texture_type, + .is_custom = is_custom, + .has_normal = HasNormalMap(), + }; +} + Framebuffer::Framebuffer(TextureRuntime& runtime, Surface* const color, u32 color_level, Surface* const depth_stencil, u32 depth_level, const Pica::Regs& regs, Common::Rectangle surfaces_rect) @@ -527,7 +628,7 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, Surface* const color, u32 colo const bool shadow_rendering = regs.framebuffer.IsShadowRendering(); const bool has_stencil = regs.framebuffer.HasStencil(); if (shadow_rendering && !color) { - return; // Framebuffer won't get used + return; } if (color) { @@ -574,19 +675,15 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, Surface* const color, u32 colo color ? color->Handle() : 0, color_level); if (depth_stencil) { if (has_stencil) { - // Attach both depth and stencil glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depth_stencil->Handle(), depth_level); } else { - // Attach depth glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_stencil->Handle(), depth_level); - // Clear stencil attachment glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); } } else { - // Clear both depth and stencil attachment glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); } diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.h b/src/video_core/renderer_opengl/gl_texture_runtime.h index 43f91af10..48fa1d68a 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.h +++ b/src/video_core/renderer_opengl/gl_texture_runtime.h @@ -10,8 +10,9 @@ #include "video_core/renderer_opengl/gl_format_reinterpreter.h" namespace VideoCore { +struct Material; class RendererBase; -} +} // namespace VideoCore namespace OpenGL { @@ -27,17 +28,19 @@ struct FormatTuple { }; struct HostTextureTag { - FormatTuple tuple{}; - VideoCore::TextureType type{}; - u32 width = 0; - u32 height = 0; - u32 levels = 1; - u32 res_scale = 1; + u32 width; + u32 height; + u32 levels; + u32 res_scale; + FormatTuple tuple; + VideoCore::TextureType type; + bool is_custom; + bool has_normal; bool operator==(const HostTextureTag& other) const noexcept { - return std::tie(tuple, type, width, height, levels, res_scale) == + return std::tie(tuple, type, width, height, levels, res_scale, is_custom, has_normal) == std::tie(other.tuple, other.type, other.width, other.height, other.levels, - other.res_scale); + other.res_scale, other.is_custom, other.has_normal); } struct Hash { @@ -46,10 +49,12 @@ struct HostTextureTag { } }; }; +static_assert(std::has_unique_object_representations_v, + "HostTextureTag is not suitable for hashing!"); struct Allocation { - std::array textures; - std::array handles; + std::array textures; + std::array handles; FormatTuple tuple; u32 width; u32 height; @@ -59,15 +64,10 @@ struct Allocation { operator bool() const noexcept { return textures[0].handle; } - - bool Matches(u32 width_, u32 height_, u32 levels_, const FormatTuple& tuple_) const { - return std::tie(width, height, levels, tuple) == std::tie(width_, height_, levels_, tuple_); - } }; class Surface; class Driver; -struct CachedTextureCube; /** * Provides texture manipulation functions to the rasterizer cache @@ -82,6 +82,9 @@ public: explicit TextureRuntime(const Driver& driver, VideoCore::RendererBase& renderer); ~TextureRuntime(); + /// Clears all cached runtime resources + void Reset(); + /// Returns true if the provided pixel format cannot be used natively by the runtime. bool NeedsConversion(VideoCore::PixelFormat pixel_format) const; @@ -90,12 +93,14 @@ public: /// Returns the OpenGL format tuple associated with the provided pixel format const FormatTuple& GetFormatTuple(VideoCore::PixelFormat pixel_format) const; + const FormatTuple& GetFormatTuple(VideoCore::CustomPixelFormat pixel_format); /// Takes back ownership of the allocation for recycling void Recycle(const HostTextureTag tag, Allocation&& alloc); - /// Allocates an OpenGL texture with the specified dimentions and format - Allocation Allocate(const VideoCore::SurfaceParams& params); + /// Allocates a texture with the specified dimentions and format + Allocation Allocate(const VideoCore::SurfaceParams& params, + const VideoCore::Material* material = nullptr); /// Fills the rectangle of the texture with the clear value provided bool ClearTexture(Surface& surface, const VideoCore::TextureClear& clear); @@ -107,7 +112,7 @@ public: bool BlitTextures(Surface& source, Surface& dest, const VideoCore::TextureBlit& blit); /// Generates mipmaps for all the available levels of the texture - void GenerateMipmaps(Surface& surface, u32 max_level); + void GenerateMipmaps(Surface& surface); /// Returns all source formats that support reinterpretation to the dest format const ReinterpreterList& GetPossibleReinterpretations(VideoCore::PixelFormat dest_format) const; @@ -123,7 +128,7 @@ private: BlitHelper blit_helper; std::vector staging_buffer; std::array reinterpreters; - std::unordered_multimap recycler; + std::unordered_multimap alloc_cache; std::unordered_map> framebuffer_cache; std::array draw_fbos; std::array read_fbos; @@ -140,9 +145,9 @@ public: Surface(Surface&& o) noexcept = default; Surface& operator=(Surface&& o) noexcept = default; - /// Returns the surface image handle - GLuint Handle(bool scaled = true) const noexcept { - return alloc.handles[static_cast(scaled)]; + /// Returns the surface image handle at the provided index. + GLuint Handle(u32 index = 1) const noexcept { + return alloc.handles[index]; } /// Returns the tuple of the surface allocation. @@ -150,9 +155,20 @@ public: return alloc.tuple; } + /// Returns the extent of the underlying surface allocation + VideoCore::Extent Extent() const noexcept { + return { + .width = alloc.width, + .height = alloc.height, + }; + } + /// Uploads pixel data in staging to a rectangle region of the surface texture void Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging); + /// Uploads the custom material to the surface allocation. + void UploadCustom(const VideoCore::Material* material, u32 level); + /// Downloads pixel data to staging from a rectangle region of the surface texture void Download(const VideoCore::BufferTextureCopy& download, const VideoCore::StagingData& staging); @@ -160,6 +176,9 @@ public: /// Attaches a handle of surface to the specified framebuffer target void Attach(GLenum target, u32 level, u32 layer, bool scaled = true); + /// Swaps the internal allocation to match the provided material + bool Swap(const VideoCore::Material* material); + /// Returns the bpp of the internal surface format u32 GetInternalBytesPerPixel() const; @@ -171,6 +190,9 @@ private: bool DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download, const VideoCore::StagingData& staging); + /// Returns the texture tag of the current allocation + HostTextureTag MakeTag() const noexcept; + private: const Driver* driver; TextureRuntime* runtime; diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 2ff11dc73..47c581bfe 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -313,7 +313,8 @@ RendererOpenGL::RendererOpenGL(Core::System& system, Frontend::EmuWindow& window } frame_dumper.mailbox = std::make_unique(); InitOpenGLObjects(); - rasterizer = std::make_unique(system.Memory(), *this, driver); + rasterizer = std::make_unique(system.Memory(), system.CustomTexManager(), + *this, driver); } RendererOpenGL::~RendererOpenGL() = default; @@ -347,6 +348,8 @@ void RendererOpenGL::SwapBuffers() { EndFrame(); prev_state.Apply(); + + rasterizer->TickFrame(); } void RendererOpenGL::RenderScreenshot() { @@ -1169,9 +1172,6 @@ void RendererOpenGL::TryPresent(int timeout_ms, bool is_secondary) { glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); } -/// Updates the framerate -void RendererOpenGL::UpdateFramerate() {} - void RendererOpenGL::PrepareVideoDumping() { auto* mailbox = static_cast(frame_dumper.mailbox.get()); { diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index 9969723f4..0b7464ad3 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -94,7 +94,6 @@ private: float h); void DrawSingleScreenStereo(const ScreenInfo& screen_info_l, const ScreenInfo& screen_info_r, float x, float y, float w, float h); - void UpdateFramerate(); // Loads framebuffer from emulated memory into the display information structure void LoadFBToScreenInfo(const GPU::Regs::FramebufferConfig& framebuffer,