Custom textures rewrite (#6452)
* common: Add thread pool from yuzu * Is really useful for asynchronous operations like shader compilation and custom textures, will be used in following PRs * core: Improve ImageInterface * Provide a default implementation so frontends don't have to duplicate code registering the lodepng version * Add a dds version too which we will use in the next commit * rasterizer_cache: Rewrite custom textures * There's just too much to talk about here, look at the PR description for more details * rasterizer_cache: Implement basic pack configuration file * custom_tex_manager: Flip dumped textures * custom_tex_manager: Optimize custom texture hashing * If no convertions are needed then we can hash the decoded data directly removing the needed for duplicate decode * custom_tex_manager: Implement asynchronous texture loading * The file loading and decoding is offloaded into worker threads, while the upload itself still occurs in the main thread to avoid having to manage shared contexts * Address review comments * custom_tex_manager: Introduce custom material support * video_core: Move custom textures to separate directory * Also split the files to make the code cleaner * gl_texture_runtime: Generate mipmaps for material * custom_tex_manager: Prevent memory overflow when preloading * externals: Add dds-ktx as submodule * string_util: Return vector from SplitString * No code benefits from passing it as an argument * custom_textures: Use json config file * gl_rasterizer: Only bind material for unit 0 * Address review comments
This commit is contained in:
parent
d16dce6d99
commit
06f3c90cfb
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -61,3 +61,6 @@
|
|||||||
[submodule "cryptopp"]
|
[submodule "cryptopp"]
|
||||||
path = externals/cryptopp
|
path = externals/cryptopp
|
||||||
url = https://github.com/weidai11/cryptopp.git
|
url = https://github.com/weidai11/cryptopp.git
|
||||||
|
[submodule "dds-ktx"]
|
||||||
|
path = externals/dds-ktx
|
||||||
|
url = https://github.com/septag/dds-ktx
|
||||||
|
11
externals/CMakeLists.txt
vendored
11
externals/CMakeLists.txt
vendored
@ -64,6 +64,10 @@ if(ANDROID)
|
|||||||
endforeach()
|
endforeach()
|
||||||
endif()
|
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
|
# fmt and Xbyak need to be added before dynarmic
|
||||||
# libfmt
|
# libfmt
|
||||||
option(FMT_INSTALL "" ON)
|
option(FMT_INSTALL "" ON)
|
||||||
@ -141,6 +145,10 @@ if (USE_DISCORD_PRESENCE)
|
|||||||
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
|
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# JSON
|
||||||
|
add_library(json-headers INTERFACE)
|
||||||
|
target_include_directories(json-headers INTERFACE ./json)
|
||||||
|
|
||||||
if (ENABLE_WEB_SERVICE)
|
if (ENABLE_WEB_SERVICE)
|
||||||
find_package(OpenSSL 1.1)
|
find_package(OpenSSL 1.1)
|
||||||
if (OPENSSL_FOUND)
|
if (OPENSSL_FOUND)
|
||||||
@ -156,9 +164,6 @@ if (ENABLE_WEB_SERVICE)
|
|||||||
DIRECTORY libressl
|
DIRECTORY libressl
|
||||||
DEFINITION OPENSSL_LIBS)
|
DEFINITION OPENSSL_LIBS)
|
||||||
endif()
|
endif()
|
||||||
# JSON
|
|
||||||
add_library(json-headers INTERFACE)
|
|
||||||
target_include_directories(json-headers INTERFACE ./json)
|
|
||||||
|
|
||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
add_subdirectory(android-ifaddrs)
|
add_subdirectory(android-ifaddrs)
|
||||||
|
1
externals/dds-ktx
vendored
Submodule
1
externals/dds-ktx
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 42dd8aa6ded90b1ec06091522774feff51e83fc5
|
2
externals/zstd
vendored
2
externals/zstd
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 63779c798237346c2b245c546c40b72a5a5913fe
|
Subproject commit e47e674cd09583ff0503f0f6defd6d23d8b718d3
|
@ -368,6 +368,7 @@ public final class SettingsFragmentPresenter {
|
|||||||
SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY);
|
SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY);
|
||||||
Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES);
|
Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES);
|
||||||
Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_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);
|
//Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES);
|
||||||
|
|
||||||
sl.add(new HeaderSetting(null, null, R.string.renderer, 0));
|
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 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_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_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.
|
//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));
|
//sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures));
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,7 @@ public final class SettingsFile {
|
|||||||
public static final String KEY_DUMP_TEXTURES = "dump_textures";
|
public static final String KEY_DUMP_TEXTURES = "dump_textures";
|
||||||
public static final String KEY_CUSTOM_TEXTURES = "custom_textures";
|
public static final String KEY_CUSTOM_TEXTURES = "custom_textures";
|
||||||
public static final String KEY_PRELOAD_TEXTURES = "preload_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_AUDIO_OUTPUT_ENGINE = "output_engine";
|
||||||
public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
|
public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
|
||||||
|
@ -25,8 +25,6 @@ add_library(citra-android SHARED
|
|||||||
game_settings.h
|
game_settings.h
|
||||||
id_cache.cpp
|
id_cache.cpp
|
||||||
id_cache.h
|
id_cache.h
|
||||||
lodepng_image_interface.cpp
|
|
||||||
lodepng_image_interface.h
|
|
||||||
mic.cpp
|
mic.cpp
|
||||||
mic.h
|
mic.h
|
||||||
native.cpp
|
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 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)
|
set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} citra-android)
|
||||||
|
@ -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(
|
JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_features_cheats_model_Cheat_isValidGatewayCode(
|
||||||
JNIEnv* env, jclass, jstring j_code) {
|
JNIEnv* env, jclass, jstring j_code) {
|
||||||
const std::string code = GetJString(env, j_code);
|
const std::string code = GetJString(env, j_code);
|
||||||
std::vector<std::string> code_lines;
|
const auto code_lines = Common::SplitString(code, '\n');
|
||||||
Common::SplitString(code, '\n', code_lines);
|
|
||||||
|
|
||||||
for (int i = 0; i < code_lines.size(); ++i) {
|
for (int i = 0; i < code_lines.size(); ++i) {
|
||||||
Cheats::GatewayCheat::CheatLine cheat_line(code_lines[i]);
|
Cheats::GatewayCheat::CheatLine cheat_line(code_lines[i]);
|
||||||
|
@ -195,6 +195,7 @@ void Config::ReadValues() {
|
|||||||
ReadSetting("Utility", Settings::values.dump_textures);
|
ReadSetting("Utility", Settings::values.dump_textures);
|
||||||
ReadSetting("Utility", Settings::values.custom_textures);
|
ReadSetting("Utility", Settings::values.custom_textures);
|
||||||
ReadSetting("Utility", Settings::values.preload_textures);
|
ReadSetting("Utility", Settings::values.preload_textures);
|
||||||
|
ReadSetting("Utility", Settings::values.async_custom_loading);
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
ReadSetting("Audio", Settings::values.audio_emulation);
|
ReadSetting("Audio", Settings::values.audio_emulation);
|
||||||
|
@ -213,6 +213,10 @@ custom_textures =
|
|||||||
# 0 (default): Off, 1: On
|
# 0 (default): Off, 1: On
|
||||||
preload_textures =
|
preload_textures =
|
||||||
|
|
||||||
|
# Loads custom textures asynchronously with background threads.
|
||||||
|
# 0: Off, 1 (default): On
|
||||||
|
async_custom_loading =
|
||||||
|
|
||||||
[Audio]
|
[Audio]
|
||||||
# Whether or not to enable DSP LLE
|
# Whether or not to enable DSP LLE
|
||||||
# 0 (default): No, 1: Yes
|
# 0 (default): No, 1: Yes
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#include <lodepng.h>
|
|
||||||
#include "common/file_util.h"
|
|
||||||
#include "common/logging/log.h"
|
|
||||||
#include "jni/lodepng_image_interface.h"
|
|
||||||
|
|
||||||
bool LodePNGImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
|
||||||
const std::string& path) {
|
|
||||||
FileUtil::IOFile file(path, "rb");
|
|
||||||
size_t read_size = file.GetSize();
|
|
||||||
std::vector<u8> in(read_size);
|
|
||||||
if (file.ReadBytes(&in[0], read_size) != read_size) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to decode {}", path);
|
|
||||||
}
|
|
||||||
u32 lodepng_ret = lodepng::decode(dst, width, height, in);
|
|
||||||
if (lodepng_ret) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to decode {} because {}", path,
|
|
||||||
lodepng_error_text(lodepng_ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LodePNGImageInterface::EncodePNG(const std::string& path, const std::vector<u8>& src,
|
|
||||||
u32 width, u32 height) {
|
|
||||||
std::vector<u8> out;
|
|
||||||
u32 lodepng_ret = lodepng::encode(out, src, width, height);
|
|
||||||
if (lodepng_ret) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to encode {} because {}", path,
|
|
||||||
lodepng_error_text(lodepng_ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
FileUtil::IOFile file(path, "wb");
|
|
||||||
if (file.WriteBytes(&out[0], out.size()) != out.size()) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to save encode to path={}", path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "core/frontend/image_interface.h"
|
|
||||||
|
|
||||||
class LodePNGImageInterface final : public Frontend::ImageInterface {
|
|
||||||
public:
|
|
||||||
bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height, const std::string& path) override;
|
|
||||||
bool EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
|
|
||||||
u32 height) override;
|
|
||||||
};
|
|
@ -39,7 +39,6 @@
|
|||||||
#include "jni/game_settings.h"
|
#include "jni/game_settings.h"
|
||||||
#include "jni/id_cache.h"
|
#include "jni/id_cache.h"
|
||||||
#include "jni/input_manager.h"
|
#include "jni/input_manager.h"
|
||||||
#include "jni/lodepng_image_interface.h"
|
|
||||||
#include "jni/mic.h"
|
#include "jni/mic.h"
|
||||||
#include "jni/native.h"
|
#include "jni/native.h"
|
||||||
#include "jni/ndk_motion.h"
|
#include "jni/ndk_motion.h"
|
||||||
@ -184,9 +183,6 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
|
|||||||
system.RegisterMiiSelector(std::make_shared<MiiSelector::AndroidMiiSelector>());
|
system.RegisterMiiSelector(std::make_shared<MiiSelector::AndroidMiiSelector>());
|
||||||
system.RegisterSoftwareKeyboard(std::make_shared<SoftwareKeyboard::AndroidKeyboard>());
|
system.RegisterSoftwareKeyboard(std::make_shared<SoftwareKeyboard::AndroidKeyboard>());
|
||||||
|
|
||||||
// Register generic image interface
|
|
||||||
Core::System::GetInstance().RegisterImageInterface(std::make_shared<LodePNGImageInterface>());
|
|
||||||
|
|
||||||
// Register real Mic factory
|
// Register real Mic factory
|
||||||
Frontend::Mic::RegisterRealMicFactory(std::make_unique<Mic::AndroidFactory>());
|
Frontend::Mic::RegisterRealMicFactory(std::make_unique<Mic::AndroidFactory>());
|
||||||
|
|
||||||
|
@ -116,6 +116,8 @@
|
|||||||
<string name="custom_textures_description">Uses custom textures found in load/textures/[GAME ID]</string>
|
<string name="custom_textures_description">Uses custom textures found in load/textures/[GAME ID]</string>
|
||||||
<string name="preload_textures">Preload custom textures</string>
|
<string name="preload_textures">Preload custom textures</string>
|
||||||
<string name="preload_textures_description">Loads all custom textures into memory. This feature can use a lot of memory.</string>
|
<string name="preload_textures_description">Loads all custom textures into memory. This feature can use a lot of memory.</string>
|
||||||
|
<string name="async_custom_loading">Async custom texture loading</string>
|
||||||
|
<string name="async_custom_loading_description">Loads custom textures in the background with worker threads to reduce loading stutter.</string>
|
||||||
<!-- Premium strings -->
|
<!-- Premium strings -->
|
||||||
<string name="premium_text">Premium</string>
|
<string name="premium_text">Premium</string>
|
||||||
<string name="premium_settings_upsell">Upgrade to Premium and support Citra!</string>
|
<string name="premium_settings_upsell">Upgrade to Premium and support Citra!</string>
|
||||||
|
@ -12,8 +12,6 @@ add_executable(citra
|
|||||||
emu_window/emu_window_sdl2_gl.h
|
emu_window/emu_window_sdl2_gl.h
|
||||||
emu_window/emu_window_sdl2_sw.cpp
|
emu_window/emu_window_sdl2_sw.cpp
|
||||||
emu_window/emu_window_sdl2_sw.h
|
emu_window/emu_window_sdl2_sw.h
|
||||||
lodepng_image_interface.cpp
|
|
||||||
lodepng_image_interface.h
|
|
||||||
precompiled_headers.h
|
precompiled_headers.h
|
||||||
resource.h
|
resource.h
|
||||||
)
|
)
|
||||||
@ -21,7 +19,7 @@ add_executable(citra
|
|||||||
create_target_directory_groups(citra)
|
create_target_directory_groups(citra)
|
||||||
|
|
||||||
target_link_libraries(citra PRIVATE common core input_common network)
|
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)
|
if (MSVC)
|
||||||
target_link_libraries(citra PRIVATE getopt)
|
target_link_libraries(citra PRIVATE getopt)
|
||||||
endif()
|
endif()
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
#include "citra/emu_window/emu_window_sdl2.h"
|
#include "citra/emu_window/emu_window_sdl2.h"
|
||||||
#include "citra/emu_window/emu_window_sdl2_gl.h"
|
#include "citra/emu_window/emu_window_sdl2_gl.h"
|
||||||
#include "citra/emu_window/emu_window_sdl2_sw.h"
|
#include "citra/emu_window/emu_window_sdl2_sw.h"
|
||||||
#include "citra/lodepng_image_interface.h"
|
|
||||||
#include "common/common_paths.h"
|
#include "common/common_paths.h"
|
||||||
#include "common/detached_tasks.h"
|
#include "common/detached_tasks.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
@ -359,9 +358,6 @@ int main(int argc, char** argv) {
|
|||||||
// Register frontend applets
|
// Register frontend applets
|
||||||
Frontend::RegisterDefaultApplets();
|
Frontend::RegisterDefaultApplets();
|
||||||
|
|
||||||
// Register generic image interface
|
|
||||||
Core::System::GetInstance().RegisterImageInterface(std::make_shared<LodePNGImageInterface>());
|
|
||||||
|
|
||||||
EmuWindow_SDL2::InitializeSDL2();
|
EmuWindow_SDL2::InitializeSDL2();
|
||||||
|
|
||||||
const auto create_emu_window = [](bool fullscreen,
|
const auto create_emu_window = [](bool fullscreen,
|
||||||
|
@ -175,6 +175,7 @@ void Config::ReadValues() {
|
|||||||
ReadSetting("Utility", Settings::values.dump_textures);
|
ReadSetting("Utility", Settings::values.dump_textures);
|
||||||
ReadSetting("Utility", Settings::values.custom_textures);
|
ReadSetting("Utility", Settings::values.custom_textures);
|
||||||
ReadSetting("Utility", Settings::values.preload_textures);
|
ReadSetting("Utility", Settings::values.preload_textures);
|
||||||
|
ReadSetting("Utility", Settings::values.async_custom_loading);
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
ReadSetting("Audio", Settings::values.audio_emulation);
|
ReadSetting("Audio", Settings::values.audio_emulation);
|
||||||
|
@ -232,6 +232,10 @@ custom_textures =
|
|||||||
# 0 (default): Off, 1: On
|
# 0 (default): Off, 1: On
|
||||||
preload_textures =
|
preload_textures =
|
||||||
|
|
||||||
|
# Loads custom textures asynchronously with background threads.
|
||||||
|
# 0: Off, 1 (default): On
|
||||||
|
async_custom_loading =
|
||||||
|
|
||||||
[Audio]
|
[Audio]
|
||||||
# Whether or not to enable DSP LLE
|
# Whether or not to enable DSP LLE
|
||||||
# 0 (default): No, 1: Yes
|
# 0 (default): No, 1: Yes
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#include <lodepng.h>
|
|
||||||
#include "citra/lodepng_image_interface.h"
|
|
||||||
#include "common/logging/log.h"
|
|
||||||
|
|
||||||
bool LodePNGImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
|
||||||
const std::string& path) {
|
|
||||||
u32 lodepng_ret = lodepng::decode(dst, width, height, path);
|
|
||||||
if (lodepng_ret) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to decode {} because {}", path,
|
|
||||||
lodepng_error_text(lodepng_ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LodePNGImageInterface::EncodePNG(const std::string& path, const std::vector<u8>& src,
|
|
||||||
u32 width, u32 height) {
|
|
||||||
u32 lodepng_ret = lodepng::encode(path, src, width, height);
|
|
||||||
if (lodepng_ret) {
|
|
||||||
LOG_CRITICAL(Frontend, "Failed to encode {} because {}", path,
|
|
||||||
lodepng_error_text(lodepng_ret));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "core/frontend/image_interface.h"
|
|
||||||
|
|
||||||
class LodePNGImageInterface final : public Frontend::ImageInterface {
|
|
||||||
public:
|
|
||||||
bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height, const std::string& path) override;
|
|
||||||
bool EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
|
|
||||||
u32 height) override;
|
|
||||||
};
|
|
@ -56,7 +56,7 @@ const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> Config:
|
|||||||
// This must be in alphabetical order according to action name as it must have the same order as
|
// 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.
|
// UISetting::values.shortcuts, which is alphabetically ordered.
|
||||||
// clang-format off
|
// clang-format off
|
||||||
const std::array<UISettings::Shortcut, 27> Config::default_hotkeys {{
|
const std::array<UISettings::Shortcut, 28> Config::default_hotkeys {{
|
||||||
{QStringLiteral("Advance Frame"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}},
|
{QStringLiteral("Advance Frame"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}},
|
||||||
{QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}},
|
{QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}},
|
||||||
{QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}},
|
{QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}},
|
||||||
@ -84,6 +84,7 @@ const std::array<UISettings::Shortcut, 27> Config::default_hotkeys {{
|
|||||||
{QStringLiteral("Toggle Screen Layout"), QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::WindowShortcut}},
|
{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 Status Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+S"), Qt::WindowShortcut}},
|
||||||
{QStringLiteral("Toggle Texture Dumping"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}},
|
{QStringLiteral("Toggle Texture Dumping"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}},
|
||||||
|
{QStringLiteral("Toggle Custom Textures"), QStringLiteral("Main Window"), {QStringLiteral("F7"), Qt::ApplicationShortcut}},
|
||||||
}};
|
}};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
@ -439,6 +440,7 @@ void Config::ReadUtilityValues() {
|
|||||||
ReadGlobalSetting(Settings::values.dump_textures);
|
ReadGlobalSetting(Settings::values.dump_textures);
|
||||||
ReadGlobalSetting(Settings::values.custom_textures);
|
ReadGlobalSetting(Settings::values.custom_textures);
|
||||||
ReadGlobalSetting(Settings::values.preload_textures);
|
ReadGlobalSetting(Settings::values.preload_textures);
|
||||||
|
ReadGlobalSetting(Settings::values.async_custom_loading);
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
@ -949,6 +951,7 @@ void Config::SaveUtilityValues() {
|
|||||||
WriteGlobalSetting(Settings::values.dump_textures);
|
WriteGlobalSetting(Settings::values.dump_textures);
|
||||||
WriteGlobalSetting(Settings::values.custom_textures);
|
WriteGlobalSetting(Settings::values.custom_textures);
|
||||||
WriteGlobalSetting(Settings::values.preload_textures);
|
WriteGlobalSetting(Settings::values.preload_textures);
|
||||||
|
WriteGlobalSetting(Settings::values.async_custom_loading);
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ public:
|
|||||||
|
|
||||||
static const std::array<int, Settings::NativeButton::NumButtons> default_buttons;
|
static const std::array<int, Settings::NativeButton::NumButtons> default_buttons;
|
||||||
static const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> default_analogs;
|
static const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> default_analogs;
|
||||||
static const std::array<UISettings::Shortcut, 27> default_hotkeys;
|
static const std::array<UISettings::Shortcut, 28> default_hotkeys;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void Initialize(const std::string& config_name);
|
void Initialize(const std::string& config_name);
|
||||||
|
@ -41,8 +41,10 @@ ConfigureEnhancements::ConfigureEnhancements(QWidget* parent)
|
|||||||
});
|
});
|
||||||
|
|
||||||
ui->toggle_preload_textures->setEnabled(ui->toggle_custom_textures->isChecked());
|
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] {
|
connect(ui->toggle_custom_textures, &QCheckBox::toggled, this, [this] {
|
||||||
ui->toggle_preload_textures->setEnabled(ui->toggle_custom_textures->isChecked());
|
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())
|
if (!ui->toggle_preload_textures->isEnabled())
|
||||||
ui->toggle_preload_textures->setChecked(false);
|
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_dump_textures->setChecked(Settings::values.dump_textures.GetValue());
|
||||||
ui->toggle_custom_textures->setChecked(Settings::values.custom_textures.GetValue());
|
ui->toggle_custom_textures->setChecked(Settings::values.custom_textures.GetValue());
|
||||||
ui->toggle_preload_textures->setChecked(Settings::values.preload_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 =
|
bg_color =
|
||||||
QColor::fromRgbF(Settings::values.bg_red.GetValue(), Settings::values.bg_green.GetValue(),
|
QColor::fromRgbF(Settings::values.bg_red.GetValue(), Settings::values.bg_green.GetValue(),
|
||||||
Settings::values.bg_blue.GetValue());
|
Settings::values.bg_blue.GetValue());
|
||||||
@ -159,6 +162,8 @@ void ConfigureEnhancements::ApplyConfiguration() {
|
|||||||
ui->toggle_custom_textures, custom_textures);
|
ui->toggle_custom_textures, custom_textures);
|
||||||
ConfigurationShared::ApplyPerGameSetting(&Settings::values.preload_textures,
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.preload_textures,
|
||||||
ui->toggle_preload_textures, 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<float>(bg_color.redF());
|
Settings::values.bg_red = static_cast<float>(bg_color.redF());
|
||||||
Settings::values.bg_green = static_cast<float>(bg_color.greenF());
|
Settings::values.bg_green = static_cast<float>(bg_color.greenF());
|
||||||
@ -176,6 +181,8 @@ void ConfigureEnhancements::SetupPerGameUI() {
|
|||||||
ui->toggle_dump_textures->setEnabled(Settings::values.dump_textures.UsingGlobal());
|
ui->toggle_dump_textures->setEnabled(Settings::values.dump_textures.UsingGlobal());
|
||||||
ui->toggle_custom_textures->setEnabled(Settings::values.custom_textures.UsingGlobal());
|
ui->toggle_custom_textures->setEnabled(Settings::values.custom_textures.UsingGlobal());
|
||||||
ui->toggle_preload_textures->setEnabled(Settings::values.preload_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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,6 +202,9 @@ void ConfigureEnhancements::SetupPerGameUI() {
|
|||||||
Settings::values.custom_textures, custom_textures);
|
Settings::values.custom_textures, custom_textures);
|
||||||
ConfigurationShared::SetColoredTristate(ui->toggle_preload_textures,
|
ConfigurationShared::SetColoredTristate(ui->toggle_preload_textures,
|
||||||
Settings::values.preload_textures, 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(
|
ConfigurationShared::SetColoredComboBox(
|
||||||
ui->resolution_factor_combobox, ui->widget_resolution,
|
ui->resolution_factor_combobox, ui->widget_resolution,
|
||||||
|
@ -44,5 +44,6 @@ private:
|
|||||||
ConfigurationShared::CheckState dump_textures;
|
ConfigurationShared::CheckState dump_textures;
|
||||||
ConfigurationShared::CheckState custom_textures;
|
ConfigurationShared::CheckState custom_textures;
|
||||||
ConfigurationShared::CheckState preload_textures;
|
ConfigurationShared::CheckState preload_textures;
|
||||||
|
ConfigurationShared::CheckState async_custom_loading;
|
||||||
QColor bg_color;
|
QColor bg_color;
|
||||||
};
|
};
|
||||||
|
@ -494,6 +494,16 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="toggle_async_custom_loading">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p>Load custom textures asynchronously with background threads to reduce loading stutter</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Async Custom Texture Loading</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -206,13 +206,11 @@ void OptionSetDialog::SetCheckBoxDefaults(const std::string& initial_value) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This is a combination of constants, splitted with + or |
|
// This is a combination of constants, splitted with + or |
|
||||||
std::vector<std::string> tmp;
|
const auto tmp = Common::SplitString(initial_value, '+');
|
||||||
Common::SplitString(initial_value, '+', tmp);
|
|
||||||
|
|
||||||
std::vector<std::string> out;
|
std::vector<std::string> out;
|
||||||
std::vector<std::string> tmp2;
|
|
||||||
for (const auto& str : tmp) {
|
for (const auto& str : tmp) {
|
||||||
Common::SplitString(str, '|', tmp2);
|
const auto tmp2 = Common::SplitString(str, '|');
|
||||||
out.insert(out.end(), tmp2.begin(), tmp2.end());
|
out.insert(out.end(), tmp2.begin(), tmp2.end());
|
||||||
}
|
}
|
||||||
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
|
for (int i = 0; i < ui->checkBoxLayout->count(); ++i) {
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include <QKeySequence>
|
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include "citra_qt/hotkeys.h"
|
#include "citra_qt/hotkeys.h"
|
||||||
#include "citra_qt/uisettings.h"
|
#include "citra_qt/uisettings.h"
|
||||||
|
|
||||||
HotkeyRegistry::HotkeyRegistry() = default;
|
HotkeyRegistry::HotkeyRegistry() = default;
|
||||||
|
|
||||||
HotkeyRegistry::~HotkeyRegistry() = default;
|
HotkeyRegistry::~HotkeyRegistry() = default;
|
||||||
|
|
||||||
void HotkeyRegistry::SaveHotkeys() {
|
void HotkeyRegistry::SaveHotkeys() {
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <QKeySequence>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
class QDialog;
|
class QDialog;
|
||||||
class QKeySequence;
|
|
||||||
class QSettings;
|
class QSettings;
|
||||||
class QShortcut;
|
class QShortcut;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
class HotkeyRegistry final {
|
class HotkeyRegistry final {
|
||||||
public:
|
public:
|
||||||
|
@ -578,6 +578,8 @@ void GMainWindow::InitializeHotkeys() {
|
|||||||
});
|
});
|
||||||
connect_shortcut(QStringLiteral("Toggle Texture Dumping"),
|
connect_shortcut(QStringLiteral("Toggle Texture Dumping"),
|
||||||
[&] { Settings::values.dump_textures = !Settings::values.dump_textures; });
|
[&] { 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
|
// 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
|
// the variable hold a garbage value after this function exits
|
||||||
static constexpr u16 SPEED_LIMIT_STEP = 5;
|
static constexpr u16 SPEED_LIMIT_STEP = 5;
|
||||||
|
@ -8,11 +8,10 @@
|
|||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
bool QtImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
bool QtImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
||||||
const std::string& path) {
|
std::span<const u8> src) {
|
||||||
QImage image(QString::fromStdString(path));
|
QImage image(QImage::fromData(src.data(), static_cast<int>(src.size())));
|
||||||
|
|
||||||
if (image.isNull()) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
width = image.width();
|
width = image.width();
|
||||||
@ -21,13 +20,15 @@ bool QtImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
|||||||
image = image.convertToFormat(QImage::Format_RGBA8888);
|
image = image.convertToFormat(QImage::Format_RGBA8888);
|
||||||
|
|
||||||
// Write RGBA8 to vector
|
// Write RGBA8 to vector
|
||||||
dst = std::vector<u8>(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QtImageInterface::EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
|
bool QtImageInterface::EncodePNG(const std::string& path, u32 width, u32 height,
|
||||||
u32 height) {
|
std::span<const u8> src) {
|
||||||
QImage image(src.data(), width, height, QImage::Format_RGBA8888);
|
QImage image(src.data(), width, height, QImage::Format_RGBA8888);
|
||||||
|
|
||||||
if (!image.save(QString::fromStdString(path), "PNG")) {
|
if (!image.save(QString::fromStdString(path), "PNG")) {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
class QtImageInterface final : public Frontend::ImageInterface {
|
class QtImageInterface final : public Frontend::ImageInterface {
|
||||||
public:
|
public:
|
||||||
bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height, const std::string& path) override;
|
bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height, std::span<const u8> src) override;
|
||||||
bool EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
|
bool EncodePNG(const std::string& path, u32 width, u32 height,
|
||||||
u32 height) override;
|
std::span<const u8> src) override;
|
||||||
};
|
};
|
||||||
|
@ -65,6 +65,8 @@ add_library(common STATIC
|
|||||||
common_precompiled_headers.h
|
common_precompiled_headers.h
|
||||||
common_types.h
|
common_types.h
|
||||||
construct.h
|
construct.h
|
||||||
|
error.cpp
|
||||||
|
error.h
|
||||||
file_util.cpp
|
file_util.cpp
|
||||||
file_util.h
|
file_util.h
|
||||||
hash.h
|
hash.h
|
||||||
@ -89,6 +91,7 @@ add_library(common STATIC
|
|||||||
misc.cpp
|
misc.cpp
|
||||||
param_package.cpp
|
param_package.cpp
|
||||||
param_package.h
|
param_package.h
|
||||||
|
polyfill_thread.h
|
||||||
precompiled_headers.h
|
precompiled_headers.h
|
||||||
quaternion.h
|
quaternion.h
|
||||||
ring_buffer.h
|
ring_buffer.h
|
||||||
@ -113,9 +116,11 @@ add_library(common STATIC
|
|||||||
thread.cpp
|
thread.cpp
|
||||||
thread.h
|
thread.h
|
||||||
thread_queue_list.h
|
thread_queue_list.h
|
||||||
|
thread_worker.h
|
||||||
threadsafe_queue.h
|
threadsafe_queue.h
|
||||||
timer.cpp
|
timer.cpp
|
||||||
timer.h
|
timer.h
|
||||||
|
unique_function.h
|
||||||
vector_math.h
|
vector_math.h
|
||||||
web_result.h
|
web_result.h
|
||||||
x64/cpu_detect.cpp
|
x64/cpu_detect.cpp
|
||||||
|
57
src/common/error.cpp
Normal file
57
src/common/error.cpp
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "common/error.h"
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
std::string NativeErrorToString(int e) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
LPSTR err_str;
|
||||||
|
|
||||||
|
DWORD res = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
||||||
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
nullptr, e, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||||
|
reinterpret_cast<LPSTR>(&err_str), 1, nullptr);
|
||||||
|
if (!res) {
|
||||||
|
return "(FormatMessageA failed to format error)";
|
||||||
|
}
|
||||||
|
std::string ret(err_str);
|
||||||
|
LocalFree(err_str);
|
||||||
|
return ret;
|
||||||
|
#else
|
||||||
|
char err_str[255];
|
||||||
|
#if defined(__GLIBC__) && (_GNU_SOURCE || (_POSIX_C_SOURCE < 200112L && _XOPEN_SOURCE < 600)) || \
|
||||||
|
defined(ANDROID)
|
||||||
|
// Thread safe (GNU-specific)
|
||||||
|
const char* str = strerror_r(e, err_str, sizeof(err_str));
|
||||||
|
return std::string(str);
|
||||||
|
#else
|
||||||
|
// Thread safe (XSI-compliant)
|
||||||
|
int second_err = strerror_r(e, err_str, sizeof(err_str));
|
||||||
|
if (second_err != 0) {
|
||||||
|
return "(strerror_r failed to format error)";
|
||||||
|
}
|
||||||
|
return std::string(err_str);
|
||||||
|
#endif // GLIBC etc.
|
||||||
|
#endif // _WIN32
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetLastErrorMsg() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return NativeErrorToString(GetLastError());
|
||||||
|
#else
|
||||||
|
return NativeErrorToString(errno);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
21
src/common/error.h
Normal file
21
src/common/error.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
// Generic function to get last error message.
|
||||||
|
// Call directly after the command or use the error num.
|
||||||
|
// This function might change the error code.
|
||||||
|
// Defined in error.cpp.
|
||||||
|
[[nodiscard]] std::string GetLastErrorMsg();
|
||||||
|
|
||||||
|
// Like GetLastErrorMsg(), but passing an explicit error code.
|
||||||
|
// Defined in error.cpp.
|
||||||
|
[[nodiscard]] std::string NativeErrorToString(int e);
|
||||||
|
|
||||||
|
} // namespace Common
|
@ -960,7 +960,7 @@ std::string_view GetFilename(std::string_view path) {
|
|||||||
const auto name_index = path.find_last_of("\\/");
|
const auto name_index = path.find_last_of("\\/");
|
||||||
|
|
||||||
if (name_index == std::string_view::npos) {
|
if (name_index == std::string_view::npos) {
|
||||||
return {};
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.substr(name_index + 1);
|
return path.substr(name_index + 1);
|
||||||
|
@ -28,12 +28,9 @@ ParamPackage::ParamPackage(const std::string& serialized) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> pairs;
|
const auto pairs = Common::SplitString(serialized, PARAM_SEPARATOR);
|
||||||
Common::SplitString(serialized, PARAM_SEPARATOR, pairs);
|
|
||||||
|
|
||||||
for (const std::string& pair : pairs) {
|
for (const std::string& pair : pairs) {
|
||||||
std::vector<std::string> key_value;
|
auto key_value = Common::SplitString(pair, KEY_VALUE_SEPARATOR);
|
||||||
Common::SplitString(pair, KEY_VALUE_SEPARATOR, key_value);
|
|
||||||
if (key_value.size() != 2) {
|
if (key_value.size() != 2) {
|
||||||
LOG_ERROR(Common, "invalid key pair {}", pair);
|
LOG_ERROR(Common, "invalid key pair {}", pair);
|
||||||
continue;
|
continue;
|
||||||
|
338
src/common/polyfill_thread.h
Normal file
338
src/common/polyfill_thread.h
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
// Copyright 2022 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
//
|
||||||
|
// TODO: remove this file when jthread is supported by all compilation targets
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <version>
|
||||||
|
|
||||||
|
#ifdef __cpp_lib_jthread
|
||||||
|
|
||||||
|
#include <stop_token>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
template <typename Condvar, typename Lock, typename Pred>
|
||||||
|
void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred&& pred) {
|
||||||
|
cv.wait(lock, token, std::move(pred));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace std {
|
||||||
|
namespace polyfill {
|
||||||
|
|
||||||
|
using stop_state_callback = size_t;
|
||||||
|
|
||||||
|
class stop_state {
|
||||||
|
public:
|
||||||
|
stop_state() = default;
|
||||||
|
~stop_state() = default;
|
||||||
|
|
||||||
|
bool request_stop() {
|
||||||
|
unique_lock lk{m_lock};
|
||||||
|
|
||||||
|
if (m_stop_requested) {
|
||||||
|
// Already set, nothing to do.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark stop requested.
|
||||||
|
m_stop_requested = true;
|
||||||
|
|
||||||
|
while (!m_callbacks.empty()) {
|
||||||
|
// Get an iterator to the first element.
|
||||||
|
const auto it = m_callbacks.begin();
|
||||||
|
|
||||||
|
// Move the callback function out of the map.
|
||||||
|
function<void()> f;
|
||||||
|
swap(it->second, f);
|
||||||
|
|
||||||
|
// Erase the now-empty map element.
|
||||||
|
m_callbacks.erase(it);
|
||||||
|
|
||||||
|
// Run the callback.
|
||||||
|
if (f) {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool stop_requested() const {
|
||||||
|
unique_lock lk{m_lock};
|
||||||
|
return m_stop_requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_state_callback insert_callback(function<void()> f) {
|
||||||
|
unique_lock lk{m_lock};
|
||||||
|
|
||||||
|
if (m_stop_requested) {
|
||||||
|
// Stop already requested. Don't insert anything,
|
||||||
|
// just run the callback synchronously.
|
||||||
|
if (f) {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the callback.
|
||||||
|
stop_state_callback ret = ++m_next_callback;
|
||||||
|
m_callbacks.emplace(ret, move(f));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove_callback(stop_state_callback cb) {
|
||||||
|
unique_lock lk{m_lock};
|
||||||
|
m_callbacks.erase(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable recursive_mutex m_lock;
|
||||||
|
map<stop_state_callback, function<void()>> m_callbacks;
|
||||||
|
stop_state_callback m_next_callback{0};
|
||||||
|
bool m_stop_requested{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace polyfill
|
||||||
|
|
||||||
|
#ifndef __cpp_lib_concepts
|
||||||
|
template <class T, class... Args>
|
||||||
|
concept constructible_from = is_nothrow_destructible_v<T> && is_constructible_v<T, Args...>;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class stop_token;
|
||||||
|
class stop_source;
|
||||||
|
struct nostopstate_t {
|
||||||
|
explicit nostopstate_t() = default;
|
||||||
|
};
|
||||||
|
inline constexpr nostopstate_t nostopstate{};
|
||||||
|
|
||||||
|
template <class Callback>
|
||||||
|
class stop_callback;
|
||||||
|
|
||||||
|
class stop_token {
|
||||||
|
public:
|
||||||
|
stop_token() noexcept = default;
|
||||||
|
|
||||||
|
stop_token(const stop_token&) noexcept = default;
|
||||||
|
stop_token(stop_token&&) noexcept = default;
|
||||||
|
stop_token& operator=(const stop_token&) noexcept = default;
|
||||||
|
stop_token& operator=(stop_token&&) noexcept = default;
|
||||||
|
~stop_token() = default;
|
||||||
|
|
||||||
|
void swap(stop_token& other) noexcept {
|
||||||
|
m_stop_state.swap(other.m_stop_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool stop_requested() const noexcept {
|
||||||
|
return m_stop_state && m_stop_state->stop_requested();
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool stop_possible() const noexcept {
|
||||||
|
return m_stop_state != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class stop_source;
|
||||||
|
template <typename Callback>
|
||||||
|
friend class stop_callback;
|
||||||
|
stop_token(shared_ptr<polyfill::stop_state> stop_state) : m_stop_state(move(stop_state)) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
shared_ptr<polyfill::stop_state> m_stop_state;
|
||||||
|
};
|
||||||
|
|
||||||
|
class stop_source {
|
||||||
|
public:
|
||||||
|
stop_source() : m_stop_state(make_shared<polyfill::stop_state>()) {}
|
||||||
|
explicit stop_source(nostopstate_t) noexcept {}
|
||||||
|
|
||||||
|
stop_source(const stop_source&) noexcept = default;
|
||||||
|
stop_source(stop_source&&) noexcept = default;
|
||||||
|
stop_source& operator=(const stop_source&) noexcept = default;
|
||||||
|
stop_source& operator=(stop_source&&) noexcept = default;
|
||||||
|
~stop_source() = default;
|
||||||
|
void swap(stop_source& other) noexcept {
|
||||||
|
m_stop_state.swap(other.m_stop_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] stop_token get_token() const noexcept {
|
||||||
|
return stop_token(m_stop_state);
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool stop_possible() const noexcept {
|
||||||
|
return m_stop_state != nullptr;
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool stop_requested() const noexcept {
|
||||||
|
return m_stop_state && m_stop_state->stop_requested();
|
||||||
|
}
|
||||||
|
bool request_stop() noexcept {
|
||||||
|
return m_stop_state && m_stop_state->request_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class jthread;
|
||||||
|
explicit stop_source(shared_ptr<polyfill::stop_state> stop_state)
|
||||||
|
: m_stop_state(move(stop_state)) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
shared_ptr<polyfill::stop_state> m_stop_state;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Callback>
|
||||||
|
class stop_callback {
|
||||||
|
static_assert(is_nothrow_destructible_v<Callback>);
|
||||||
|
static_assert(is_invocable_v<Callback>);
|
||||||
|
|
||||||
|
public:
|
||||||
|
using callback_type = Callback;
|
||||||
|
|
||||||
|
template <typename C>
|
||||||
|
requires constructible_from<Callback, C>
|
||||||
|
explicit stop_callback(const stop_token& st,
|
||||||
|
C&& cb) noexcept(is_nothrow_constructible_v<Callback, C>)
|
||||||
|
: m_stop_state(st.m_stop_state) {
|
||||||
|
if (m_stop_state) {
|
||||||
|
m_callback = m_stop_state->insert_callback(move(cb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template <typename C>
|
||||||
|
requires constructible_from<Callback, C>
|
||||||
|
explicit stop_callback(stop_token&& st,
|
||||||
|
C&& cb) noexcept(is_nothrow_constructible_v<Callback, C>)
|
||||||
|
: m_stop_state(move(st.m_stop_state)) {
|
||||||
|
if (m_stop_state) {
|
||||||
|
m_callback = m_stop_state->insert_callback(move(cb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
~stop_callback() {
|
||||||
|
if (m_stop_state && m_callback) {
|
||||||
|
m_stop_state->remove_callback(m_callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_callback(const stop_callback&) = delete;
|
||||||
|
stop_callback(stop_callback&&) = delete;
|
||||||
|
stop_callback& operator=(const stop_callback&) = delete;
|
||||||
|
stop_callback& operator=(stop_callback&&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
shared_ptr<polyfill::stop_state> m_stop_state;
|
||||||
|
polyfill::stop_state_callback m_callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Callback>
|
||||||
|
stop_callback(stop_token, Callback) -> stop_callback<Callback>;
|
||||||
|
|
||||||
|
class jthread {
|
||||||
|
public:
|
||||||
|
using id = thread::id;
|
||||||
|
using native_handle_type = thread::native_handle_type;
|
||||||
|
|
||||||
|
jthread() noexcept = default;
|
||||||
|
|
||||||
|
template <typename F, typename... Args,
|
||||||
|
typename = enable_if_t<!is_same_v<remove_cvref_t<F>, jthread>>>
|
||||||
|
explicit jthread(F&& f, Args&&... args)
|
||||||
|
: m_stop_state(make_shared<polyfill::stop_state>()),
|
||||||
|
m_thread(make_thread(move(f), move(args)...)) {}
|
||||||
|
|
||||||
|
~jthread() {
|
||||||
|
if (joinable()) {
|
||||||
|
request_stop();
|
||||||
|
join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jthread(const jthread&) = delete;
|
||||||
|
jthread(jthread&&) noexcept = default;
|
||||||
|
jthread& operator=(const jthread&) = delete;
|
||||||
|
|
||||||
|
jthread& operator=(jthread&& other) noexcept {
|
||||||
|
m_thread.swap(other.m_thread);
|
||||||
|
m_stop_state.swap(other.m_stop_state);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void swap(jthread& other) noexcept {
|
||||||
|
m_thread.swap(other.m_thread);
|
||||||
|
m_stop_state.swap(other.m_stop_state);
|
||||||
|
}
|
||||||
|
[[nodiscard]] bool joinable() const noexcept {
|
||||||
|
return m_thread.joinable();
|
||||||
|
}
|
||||||
|
void join() {
|
||||||
|
m_thread.join();
|
||||||
|
}
|
||||||
|
void detach() {
|
||||||
|
m_thread.detach();
|
||||||
|
m_stop_state.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] id get_id() const noexcept {
|
||||||
|
return m_thread.get_id();
|
||||||
|
}
|
||||||
|
[[nodiscard]] native_handle_type native_handle() {
|
||||||
|
return m_thread.native_handle();
|
||||||
|
}
|
||||||
|
[[nodiscard]] stop_source get_stop_source() noexcept {
|
||||||
|
return stop_source(m_stop_state);
|
||||||
|
}
|
||||||
|
[[nodiscard]] stop_token get_stop_token() const noexcept {
|
||||||
|
return stop_source(m_stop_state).get_token();
|
||||||
|
}
|
||||||
|
bool request_stop() noexcept {
|
||||||
|
return get_stop_source().request_stop();
|
||||||
|
}
|
||||||
|
[[nodiscard]] static unsigned int hardware_concurrency() noexcept {
|
||||||
|
return thread::hardware_concurrency();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename F, typename... Args>
|
||||||
|
thread make_thread(F&& f, Args&&... args) {
|
||||||
|
if constexpr (is_invocable_v<decay_t<F>, stop_token, decay_t<Args>...>) {
|
||||||
|
return thread(move(f), get_stop_token(), move(args)...);
|
||||||
|
} else {
|
||||||
|
return thread(move(f), move(args)...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_ptr<polyfill::stop_state> m_stop_state;
|
||||||
|
thread m_thread;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace std
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
template <typename Condvar, typename Lock, typename Pred>
|
||||||
|
void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred pred) {
|
||||||
|
if (token.stop_requested()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stop_callback callback(token, [&] { cv.notify_all(); });
|
||||||
|
cv.wait(lock, [&] { return pred() || token.stop_requested(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
|
|
||||||
|
#endif // __cpp_lib_jthread
|
@ -477,6 +477,7 @@ struct Values {
|
|||||||
SwitchableSetting<bool> dump_textures{false, "dump_textures"};
|
SwitchableSetting<bool> dump_textures{false, "dump_textures"};
|
||||||
SwitchableSetting<bool> custom_textures{false, "custom_textures"};
|
SwitchableSetting<bool> custom_textures{false, "custom_textures"};
|
||||||
SwitchableSetting<bool> preload_textures{false, "preload_textures"};
|
SwitchableSetting<bool> preload_textures{false, "preload_textures"};
|
||||||
|
SwitchableSetting<bool> async_custom_loading{true, "async_custom_loading"};
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
bool audio_muted;
|
bool audio_muted;
|
||||||
|
@ -102,15 +102,16 @@ void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _P
|
|||||||
_CompleteFilename += _Filename;
|
_CompleteFilename += _Filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SplitString(const std::string& str, const char delim, std::vector<std::string>& output) {
|
std::vector<std::string> SplitString(const std::string& str, const char delim) {
|
||||||
std::istringstream iss(str);
|
std::istringstream iss(str);
|
||||||
output.resize(1);
|
std::vector<std::string> output(1);
|
||||||
|
|
||||||
while (std::getline(iss, *output.rbegin(), delim)) {
|
while (std::getline(iss, *output.rbegin(), delim)) {
|
||||||
output.emplace_back();
|
output.emplace_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
output.pop_back();
|
output.pop_back();
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TabsToSpaces(int tab_size, std::string in) {
|
std::string TabsToSpaces(int tab_size, std::string in) {
|
||||||
|
@ -29,7 +29,7 @@ namespace Common {
|
|||||||
|
|
||||||
[[nodiscard]] bool EndsWith(const std::string& value, const std::string& ending);
|
[[nodiscard]] bool EndsWith(const std::string& value, const std::string& ending);
|
||||||
|
|
||||||
void SplitString(const std::string& str, char delim, std::vector<std::string>& output);
|
[[nodiscard]] std::vector<std::string> SplitString(const std::string& str, const char delim);
|
||||||
|
|
||||||
// "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe"
|
// "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe"
|
||||||
bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _pFilename,
|
bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _pFilename,
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/common_types.h"
|
#include "common/texture.h"
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
void FlipRGBA8Texture(std::vector<u8>& tex, u32 width, u32 height) {
|
|
||||||
|
void FlipRGBA8Texture(std::span<u8> tex, u32 width, u32 height) {
|
||||||
ASSERT(tex.size() == width * height * 4);
|
ASSERT(tex.size() == width * height * 4);
|
||||||
const u32 line_size = width * 4;
|
const u32 line_size = width * 4;
|
||||||
for (u32 line = 0; line < height / 2; line++) {
|
for (u32 line = 0; line < height / 2; line++) {
|
||||||
@ -19,4 +19,5 @@ void FlipRGBA8Texture(std::vector<u8>& tex, u32 width, u32 height) {
|
|||||||
tex.begin() + offset_2);
|
tex.begin() + offset_2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <vector>
|
#include <span>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
void FlipRGBA8Texture(std::vector<u8>& tex, u32 width, u32 height);
|
|
||||||
}
|
void FlipRGBA8Texture(std::span<u8> tex, u32 width, u32 height);
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "common/common_funcs.h"
|
#include <string>
|
||||||
|
|
||||||
|
#include "common/error.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/thread.h"
|
#include "common/thread.h"
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
@ -20,7 +22,6 @@
|
|||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#ifdef __FreeBSD__
|
#ifdef __FreeBSD__
|
||||||
#define cpu_set_t cpuset_t
|
#define cpu_set_t cpuset_t
|
||||||
@ -28,6 +29,56 @@
|
|||||||
|
|
||||||
namespace Common {
|
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<u32>(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
|
#ifdef _MSC_VER
|
||||||
|
|
||||||
// Sets the debugger-visible name of the current thread.
|
// Sets the debugger-visible name of the current thread.
|
||||||
@ -47,7 +98,7 @@ void SetCurrentThreadName(const char* name) {
|
|||||||
|
|
||||||
info.dwType = 0x1000;
|
info.dwType = 0x1000;
|
||||||
info.szName = name;
|
info.szName = name;
|
||||||
info.dwThreadID = static_cast<DWORD>(-1);
|
info.dwThreadID = std::numeric_limits<DWORD>::max();
|
||||||
info.dwFlags = 0;
|
info.dwFlags = 0;
|
||||||
|
|
||||||
__try {
|
__try {
|
||||||
@ -81,6 +132,12 @@ void SetCurrentThreadName(const char* name) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
void SetCurrentThreadName(const char*) {
|
||||||
|
// Do Nothing on MingW
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
@ -10,13 +10,15 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/polyfill_thread.h"
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
class Event {
|
class Event {
|
||||||
public:
|
public:
|
||||||
void Set() {
|
void Set() {
|
||||||
std::lock_guard lk{mutex};
|
std::scoped_lock lk{mutex};
|
||||||
if (!is_set) {
|
if (!is_set) {
|
||||||
is_set = true;
|
is_set = true;
|
||||||
condvar.notify_one();
|
condvar.notify_one();
|
||||||
@ -54,6 +56,10 @@ public:
|
|||||||
is_set = false;
|
is_set = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsSet() {
|
||||||
|
return is_set;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::condition_variable condvar;
|
std::condition_variable condvar;
|
||||||
std::mutex mutex;
|
std::mutex mutex;
|
||||||
@ -65,7 +71,7 @@ public:
|
|||||||
explicit Barrier(std::size_t count_) : count(count_) {}
|
explicit Barrier(std::size_t count_) : count(count_) {}
|
||||||
|
|
||||||
/// Blocks until all "count" threads have called Sync()
|
/// Blocks until all "count" threads have called Sync()
|
||||||
void Sync() {
|
bool Sync(std::stop_token token = {}) {
|
||||||
std::unique_lock lk{mutex};
|
std::unique_lock lk{mutex};
|
||||||
const std::size_t current_generation = generation;
|
const std::size_t current_generation = generation;
|
||||||
|
|
||||||
@ -73,25 +79,37 @@ public:
|
|||||||
generation++;
|
generation++;
|
||||||
waiting = 0;
|
waiting = 0;
|
||||||
condvar.notify_all();
|
condvar.notify_all();
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
condvar.wait(lk,
|
CondvarWait(condvar, lk, token,
|
||||||
[this, current_generation] { return current_generation != generation; });
|
[this, current_generation] { return current_generation != generation; });
|
||||||
|
return !token.stop_requested();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t Generation() const {
|
std::size_t Generation() {
|
||||||
std::unique_lock lk(mutex);
|
std::unique_lock lk{mutex};
|
||||||
return generation;
|
return generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::condition_variable condvar;
|
std::condition_variable_any condvar;
|
||||||
mutable std::mutex mutex;
|
std::mutex mutex;
|
||||||
std::size_t count;
|
std::size_t count;
|
||||||
std::size_t waiting = 0;
|
std::size_t waiting = 0;
|
||||||
std::size_t generation = 0; // Incremented once each time the barrier is used
|
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);
|
void SetCurrentThreadName(const char* name);
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
123
src/common/thread_worker.h
Normal file
123
src/common/thread_worker.h
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// Copyright 2020 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
#include "common/polyfill_thread.h"
|
||||||
|
#include "common/thread.h"
|
||||||
|
#include "common/unique_function.h"
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
template <class StateType = void>
|
||||||
|
class StatefulThreadWorker {
|
||||||
|
static constexpr bool with_state = !std::is_same_v<StateType, void>;
|
||||||
|
|
||||||
|
struct DummyCallable {
|
||||||
|
int operator()(size_t) const noexcept {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using Task =
|
||||||
|
std::conditional_t<with_state, UniqueFunction<void, StateType*>, UniqueFunction<void>>;
|
||||||
|
using StateMaker =
|
||||||
|
std::conditional_t<with_state, std::function<StateType(size_t)>, DummyCallable>;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit StatefulThreadWorker(size_t num_workers, std::string_view name, StateMaker func = {})
|
||||||
|
: workers_queued{num_workers}, thread_name{name} {
|
||||||
|
const auto lambda = [this, func](std::stop_token stop_token, size_t index) {
|
||||||
|
Common::SetCurrentThreadName(thread_name.data());
|
||||||
|
{
|
||||||
|
[[maybe_unused]] std::conditional_t<with_state, StateType, int> state{func(index)};
|
||||||
|
while (!stop_token.stop_requested()) {
|
||||||
|
Task task;
|
||||||
|
{
|
||||||
|
std::unique_lock lock{queue_mutex};
|
||||||
|
if (requests.empty()) {
|
||||||
|
wait_condition.notify_all();
|
||||||
|
}
|
||||||
|
Common::CondvarWait(condition, lock, stop_token,
|
||||||
|
[this] { return !requests.empty(); });
|
||||||
|
if (stop_token.stop_requested()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
task = std::move(requests.front());
|
||||||
|
requests.pop();
|
||||||
|
}
|
||||||
|
if constexpr (with_state) {
|
||||||
|
task(&state);
|
||||||
|
} else {
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
++work_done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++workers_stopped;
|
||||||
|
wait_condition.notify_all();
|
||||||
|
};
|
||||||
|
threads.reserve(num_workers);
|
||||||
|
for (size_t i = 0; i < num_workers; ++i) {
|
||||||
|
threads.emplace_back(lambda, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatefulThreadWorker& operator=(const StatefulThreadWorker&) = delete;
|
||||||
|
StatefulThreadWorker(const StatefulThreadWorker&) = delete;
|
||||||
|
|
||||||
|
StatefulThreadWorker& operator=(StatefulThreadWorker&&) = delete;
|
||||||
|
StatefulThreadWorker(StatefulThreadWorker&&) = delete;
|
||||||
|
|
||||||
|
void QueueWork(Task work) {
|
||||||
|
{
|
||||||
|
std::unique_lock lock{queue_mutex};
|
||||||
|
requests.emplace(std::move(work));
|
||||||
|
++work_scheduled;
|
||||||
|
}
|
||||||
|
condition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaitForRequests(std::stop_token stop_token = {}) {
|
||||||
|
std::stop_callback callback(stop_token, [this] {
|
||||||
|
for (auto& thread : threads) {
|
||||||
|
thread.request_stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
std::unique_lock lock{queue_mutex};
|
||||||
|
wait_condition.wait(lock, [this] {
|
||||||
|
return workers_stopped >= workers_queued || work_done >= work_scheduled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t NumWorkers() const noexcept {
|
||||||
|
return threads.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::queue<Task> requests;
|
||||||
|
std::mutex queue_mutex;
|
||||||
|
std::condition_variable_any condition;
|
||||||
|
std::condition_variable wait_condition;
|
||||||
|
std::atomic<size_t> work_scheduled{};
|
||||||
|
std::atomic<size_t> work_done{};
|
||||||
|
std::atomic<size_t> workers_stopped{};
|
||||||
|
std::atomic<size_t> workers_queued{};
|
||||||
|
std::string_view thread_name;
|
||||||
|
std::vector<std::jthread> threads;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ThreadWorker = StatefulThreadWorker<>;
|
||||||
|
|
||||||
|
} // namespace Common
|
62
src/common/unique_function.h
Normal file
62
src/common/unique_function.h
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2021 yuzu Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace Common {
|
||||||
|
|
||||||
|
/// General purpose function wrapper similar to std::function.
|
||||||
|
/// Unlike std::function, the captured values don't have to be copyable.
|
||||||
|
/// This class can be moved but not copied.
|
||||||
|
template <typename ResultType, typename... Args>
|
||||||
|
class UniqueFunction {
|
||||||
|
class CallableBase {
|
||||||
|
public:
|
||||||
|
virtual ~CallableBase() = default;
|
||||||
|
virtual ResultType operator()(Args&&...) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Functor>
|
||||||
|
class Callable final : public CallableBase {
|
||||||
|
public:
|
||||||
|
Callable(Functor&& functor_) : functor{std::move(functor_)} {}
|
||||||
|
~Callable() override = default;
|
||||||
|
|
||||||
|
ResultType operator()(Args&&... args) override {
|
||||||
|
return functor(std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Functor functor;
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
UniqueFunction() = default;
|
||||||
|
|
||||||
|
template <typename Functor>
|
||||||
|
UniqueFunction(Functor&& functor)
|
||||||
|
: callable{std::make_unique<Callable<Functor>>(std::move(functor))} {}
|
||||||
|
|
||||||
|
UniqueFunction& operator=(UniqueFunction&& rhs) noexcept = default;
|
||||||
|
UniqueFunction(UniqueFunction&& rhs) noexcept = default;
|
||||||
|
|
||||||
|
UniqueFunction& operator=(const UniqueFunction&) = delete;
|
||||||
|
UniqueFunction(const UniqueFunction&) = delete;
|
||||||
|
|
||||||
|
ResultType operator()(Args&&... args) const {
|
||||||
|
return (*callable)(std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit operator bool() const noexcept {
|
||||||
|
return static_cast<bool>(callable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<CallableBase> callable;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Common
|
@ -36,8 +36,6 @@ add_library(core STATIC
|
|||||||
core.h
|
core.h
|
||||||
core_timing.cpp
|
core_timing.cpp
|
||||||
core_timing.h
|
core_timing.h
|
||||||
custom_tex_cache.cpp
|
|
||||||
custom_tex_cache.h
|
|
||||||
dumping/backend.cpp
|
dumping/backend.cpp
|
||||||
dumping/backend.h
|
dumping/backend.h
|
||||||
file_sys/archive_backend.cpp
|
file_sys/archive_backend.cpp
|
||||||
@ -109,6 +107,7 @@ add_library(core STATIC
|
|||||||
frontend/emu_window.h
|
frontend/emu_window.h
|
||||||
frontend/framebuffer_layout.cpp
|
frontend/framebuffer_layout.cpp
|
||||||
frontend/framebuffer_layout.h
|
frontend/framebuffer_layout.h
|
||||||
|
frontend/image_interface.cpp
|
||||||
frontend/image_interface.h
|
frontend/image_interface.h
|
||||||
frontend/input.h
|
frontend/input.h
|
||||||
frontend/mic.cpp
|
frontend/mic.cpp
|
||||||
@ -481,7 +480,8 @@ endif()
|
|||||||
create_target_directory_groups(core)
|
create_target_directory_groups(core)
|
||||||
|
|
||||||
target_link_libraries(core PUBLIC common PRIVATE audio_core network video_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})
|
set_target_properties(core PROPERTIES INTERPROCEDURAL_OPTIMIZATION ${ENABLE_LTO})
|
||||||
|
|
||||||
if (ENABLE_WEB_SERVICE)
|
if (ENABLE_WEB_SERVICE)
|
||||||
|
@ -216,13 +216,12 @@ GatewayCheat::GatewayCheat(std::string name_, std::vector<CheatLine> cheat_lines
|
|||||||
GatewayCheat::GatewayCheat(std::string name_, std::string code, std::string comments_)
|
GatewayCheat::GatewayCheat(std::string name_, std::string code, std::string comments_)
|
||||||
: name(std::move(name_)), comments(std::move(comments_)) {
|
: name(std::move(name_)), comments(std::move(comments_)) {
|
||||||
|
|
||||||
std::vector<std::string> code_lines;
|
const auto code_lines = Common::SplitString(code, '\n');
|
||||||
Common::SplitString(code, '\n', code_lines);
|
|
||||||
|
|
||||||
std::vector<CheatLine> temp_cheat_lines;
|
std::vector<CheatLine> temp_cheat_lines;
|
||||||
for (std::size_t i = 0; i < code_lines.size(); ++i) {
|
for (const std::string& line : code_lines) {
|
||||||
if (!code_lines[i].empty())
|
if (!line.empty())
|
||||||
temp_cheat_lines.emplace_back(code_lines[i]);
|
temp_cheat_lines.emplace_back(line);
|
||||||
}
|
}
|
||||||
cheat_lines = std::move(temp_cheat_lines);
|
cheat_lines = std::move(temp_cheat_lines);
|
||||||
}
|
}
|
||||||
@ -464,10 +463,10 @@ std::string GatewayCheat::ToString() const {
|
|||||||
result += EnabledText;
|
result += EnabledText;
|
||||||
result += '\n';
|
result += '\n';
|
||||||
}
|
}
|
||||||
std::vector<std::string> comment_lines;
|
const auto comment_lines = Common::SplitString(comments, '\n');
|
||||||
Common::SplitString(comments, '\n', comment_lines);
|
for (const auto& comment_line : comment_lines) {
|
||||||
for (const auto& comment_line : comment_lines)
|
|
||||||
result += "*" + comment_line + '\n';
|
result += "*" + comment_line + '\n';
|
||||||
|
}
|
||||||
result += GetCode() + '\n';
|
result += GetCode() + '\n';
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
#include "core/dumping/ffmpeg_backend.h"
|
#include "core/dumping/ffmpeg_backend.h"
|
||||||
#endif
|
#endif
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "core/custom_tex_cache.h"
|
#include "core/frontend/image_interface.h"
|
||||||
#include "core/gdbstub/gdbstub.h"
|
#include "core/gdbstub/gdbstub.h"
|
||||||
#include "core/global.h"
|
#include "core/global.h"
|
||||||
#include "core/hle/kernel/client_port.h"
|
#include "core/hle/kernel/client_port.h"
|
||||||
@ -48,6 +48,7 @@
|
|||||||
#include "core/movie.h"
|
#include "core/movie.h"
|
||||||
#include "core/rpc/rpc_server.h"
|
#include "core/rpc/rpc_server.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
|
#include "video_core/custom_textures/custom_tex_manager.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
@ -318,16 +319,15 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
|
|||||||
static_cast<u32>(load_result));
|
static_cast<u32>(load_result));
|
||||||
}
|
}
|
||||||
perf_stats = std::make_unique<PerfStats>(title_id);
|
perf_stats = std::make_unique<PerfStats>(title_id);
|
||||||
custom_tex_cache = std::make_unique<Core::CustomTexCache>();
|
|
||||||
|
|
||||||
if (Settings::values.custom_textures) {
|
if (Settings::values.custom_textures) {
|
||||||
const u64 program_id = Kernel().GetCurrentProcess()->codeset->program_id;
|
custom_tex_manager->FindCustomTextures();
|
||||||
FileUtil::CreateFullPath(fmt::format(
|
|
||||||
"{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), program_id));
|
|
||||||
custom_tex_cache->FindCustomTextures(program_id);
|
|
||||||
}
|
}
|
||||||
if (Settings::values.preload_textures) {
|
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;
|
status = ResultStatus::Success;
|
||||||
@ -432,6 +432,12 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window,
|
|||||||
video_dumper = std::make_unique<VideoDumper::NullBackend>();
|
video_dumper = std::make_unique<VideoDumper::NullBackend>();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (!registered_image_interface) {
|
||||||
|
registered_image_interface = std::make_shared<Frontend::ImageInterface>();
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_tex_manager = std::make_unique<VideoCore::CustomTexManager>(*this);
|
||||||
|
|
||||||
VideoCore::Init(emu_window, secondary_window, *this);
|
VideoCore::Init(emu_window, secondary_window, *this);
|
||||||
|
|
||||||
LOG_DEBUG(Core, "Initialized OK");
|
LOG_DEBUG(Core, "Initialized OK");
|
||||||
@ -505,12 +511,12 @@ const VideoDumper::Backend& System::VideoDumper() const {
|
|||||||
return *video_dumper;
|
return *video_dumper;
|
||||||
}
|
}
|
||||||
|
|
||||||
Core::CustomTexCache& System::CustomTexCache() {
|
VideoCore::CustomTexManager& System::CustomTexManager() {
|
||||||
return *custom_tex_cache;
|
return *custom_tex_manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Core::CustomTexCache& System::CustomTexCache() const {
|
const VideoCore::CustomTexManager& System::CustomTexManager() const {
|
||||||
return *custom_tex_cache;
|
return *custom_tex_manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
void System::RegisterMiiSelector(std::shared_ptr<Frontend::MiiSelector> mii_selector) {
|
void System::RegisterMiiSelector(std::shared_ptr<Frontend::MiiSelector> mii_selector) {
|
||||||
|
@ -10,10 +10,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <boost/serialization/version.hpp>
|
#include <boost/serialization/version.hpp>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/custom_tex_cache.h"
|
|
||||||
#include "core/frontend/applets/mii_selector.h"
|
#include "core/frontend/applets/mii_selector.h"
|
||||||
#include "core/frontend/applets/swkbd.h"
|
#include "core/frontend/applets/swkbd.h"
|
||||||
#include "core/frontend/image_interface.h"
|
|
||||||
#include "core/loader/loader.h"
|
#include "core/loader/loader.h"
|
||||||
#include "core/memory.h"
|
#include "core/memory.h"
|
||||||
#include "core/perf_stats.h"
|
#include "core/perf_stats.h"
|
||||||
@ -23,7 +21,8 @@ class ARM_Interface;
|
|||||||
|
|
||||||
namespace Frontend {
|
namespace Frontend {
|
||||||
class EmuWindow;
|
class EmuWindow;
|
||||||
}
|
class ImageInterface;
|
||||||
|
} // namespace Frontend
|
||||||
|
|
||||||
namespace Memory {
|
namespace Memory {
|
||||||
class MemorySystem;
|
class MemorySystem;
|
||||||
@ -59,8 +58,9 @@ class Backend;
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace VideoCore {
|
namespace VideoCore {
|
||||||
|
class CustomTexManager;
|
||||||
class RendererBase;
|
class RendererBase;
|
||||||
}
|
} // namespace VideoCore
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
|
||||||
@ -253,10 +253,10 @@ public:
|
|||||||
[[nodiscard]] const Cheats::CheatEngine& CheatEngine() const;
|
[[nodiscard]] const Cheats::CheatEngine& CheatEngine() const;
|
||||||
|
|
||||||
/// Gets a reference to the custom texture cache system
|
/// 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
|
/// 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
|
/// Gets a reference to the video dumper backend
|
||||||
[[nodiscard]] VideoDumper::Backend& VideoDumper();
|
[[nodiscard]] VideoDumper::Backend& VideoDumper();
|
||||||
@ -362,7 +362,7 @@ private:
|
|||||||
std::unique_ptr<VideoDumper::Backend> video_dumper;
|
std::unique_ptr<VideoDumper::Backend> video_dumper;
|
||||||
|
|
||||||
/// Custom texture cache system
|
/// Custom texture cache system
|
||||||
std::unique_ptr<Core::CustomTexCache> custom_tex_cache;
|
std::unique_ptr<VideoCore::CustomTexManager> custom_tex_manager;
|
||||||
|
|
||||||
/// Image interface
|
/// Image interface
|
||||||
std::shared_ptr<Frontend::ImageInterface> registered_image_interface;
|
std::shared_ptr<Frontend::ImageInterface> registered_image_interface;
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#include <fmt/format.h>
|
|
||||||
#include "common/file_util.h"
|
|
||||||
#include "common/texture.h"
|
|
||||||
#include "core.h"
|
|
||||||
#include "core/custom_tex_cache.h"
|
|
||||||
|
|
||||||
namespace Core {
|
|
||||||
CustomTexCache::CustomTexCache() = default;
|
|
||||||
|
|
||||||
CustomTexCache::~CustomTexCache() = default;
|
|
||||||
|
|
||||||
bool CustomTexCache::IsTextureDumped(u64 hash) const {
|
|
||||||
return dumped_textures.count(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CustomTexCache::SetTextureDumped(const u64 hash) {
|
|
||||||
dumped_textures.insert(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CustomTexCache::IsTextureCached(u64 hash) const {
|
|
||||||
return custom_textures.count(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomTexInfo& CustomTexCache::LookupTexture(u64 hash) const {
|
|
||||||
return custom_textures.at(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CustomTexCache::CacheTexture(u64 hash, const std::vector<u8>& tex, u32 width, u32 height) {
|
|
||||||
custom_textures[hash] = {width, height, tex};
|
|
||||||
}
|
|
||||||
|
|
||||||
void CustomTexCache::AddTexturePath(u64 hash, const std::string& path) {
|
|
||||||
if (custom_texture_paths.count(hash))
|
|
||||||
LOG_ERROR(Core, "Textures {} and {} conflict!", custom_texture_paths[hash].path, path);
|
|
||||||
else
|
|
||||||
custom_texture_paths[hash] = {path, hash};
|
|
||||||
}
|
|
||||||
|
|
||||||
void CustomTexCache::FindCustomTextures(u64 program_id) {
|
|
||||||
// Custom textures are currently stored as
|
|
||||||
// [TitleID]/tex1_[width]x[height]_[64-bit hash]_[format].png
|
|
||||||
|
|
||||||
const std::string load_path = fmt::format(
|
|
||||||
"{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), program_id);
|
|
||||||
|
|
||||||
if (FileUtil::Exists(load_path)) {
|
|
||||||
FileUtil::FSTEntry texture_dir;
|
|
||||||
std::vector<FileUtil::FSTEntry> textures;
|
|
||||||
// 64 nested folders should be plenty for most cases
|
|
||||||
FileUtil::ScanDirectoryTree(load_path, texture_dir, 64);
|
|
||||||
FileUtil::GetAllFilesFromNestedEntries(texture_dir, textures);
|
|
||||||
|
|
||||||
for (const auto& file : textures) {
|
|
||||||
if (file.isDirectory)
|
|
||||||
continue;
|
|
||||||
if (file.virtualName.substr(0, 5) != "tex1_")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
u32 width;
|
|
||||||
u32 height;
|
|
||||||
u64 hash;
|
|
||||||
u32 format; // unused
|
|
||||||
// TODO: more modern way of doing this
|
|
||||||
if (std::sscanf(file.virtualName.c_str(), "tex1_%ux%u_%llX_%u.png", &width, &height,
|
|
||||||
&hash, &format) == 4) {
|
|
||||||
AddTexturePath(hash, file.physicalName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CustomTexCache::PreloadTextures(Frontend::ImageInterface& image_interface) {
|
|
||||||
for (const auto& path : custom_texture_paths) {
|
|
||||||
const auto& path_info = path.second;
|
|
||||||
Core::CustomTexInfo tex_info;
|
|
||||||
if (image_interface.DecodePNG(tex_info.tex, tex_info.width, tex_info.height,
|
|
||||||
path_info.path)) {
|
|
||||||
// Make sure the texture size is a power of 2
|
|
||||||
std::bitset<32> width_bits(tex_info.width);
|
|
||||||
std::bitset<32> height_bits(tex_info.height);
|
|
||||||
if (width_bits.count() == 1 && height_bits.count() == 1) {
|
|
||||||
LOG_DEBUG(Render_OpenGL, "Loaded custom texture from {}", path_info.path);
|
|
||||||
Common::FlipRGBA8Texture(tex_info.tex, tex_info.width, tex_info.height);
|
|
||||||
CacheTexture(path_info.hash, tex_info.tex, tex_info.width, tex_info.height);
|
|
||||||
} else {
|
|
||||||
LOG_ERROR(Render_OpenGL, "Texture {} size is not a power of 2", path_info.path);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_ERROR(Render_OpenGL, "Failed to load custom texture {}", path_info.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CustomTexCache::CustomTextureExists(u64 hash) const {
|
|
||||||
return custom_texture_paths.count(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomTexPathInfo& CustomTexCache::LookupTexturePathInfo(u64 hash) const {
|
|
||||||
return custom_texture_paths.at(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CustomTexCache::IsTexturePathMapEmpty() const {
|
|
||||||
return custom_texture_paths.size() == 0;
|
|
||||||
}
|
|
||||||
} // namespace Core
|
|
@ -1,55 +0,0 @@
|
|||||||
// Copyright 2019 Citra Emulator Project
|
|
||||||
// Licensed under GPLv2 or any later version
|
|
||||||
// Refer to the license.txt file included.
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
|
||||||
#include "common/common_types.h"
|
|
||||||
|
|
||||||
namespace Frontend {
|
|
||||||
class ImageInterface;
|
|
||||||
} // namespace Frontend
|
|
||||||
|
|
||||||
namespace Core {
|
|
||||||
struct CustomTexInfo {
|
|
||||||
u32 width;
|
|
||||||
u32 height;
|
|
||||||
std::vector<u8> tex;
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is to avoid parsing the filename multiple times
|
|
||||||
struct CustomTexPathInfo {
|
|
||||||
std::string path;
|
|
||||||
u64 hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: think of a better name for this class...
|
|
||||||
class CustomTexCache {
|
|
||||||
public:
|
|
||||||
explicit CustomTexCache();
|
|
||||||
~CustomTexCache();
|
|
||||||
|
|
||||||
bool IsTextureDumped(u64 hash) const;
|
|
||||||
void SetTextureDumped(u64 hash);
|
|
||||||
|
|
||||||
bool IsTextureCached(u64 hash) const;
|
|
||||||
const CustomTexInfo& LookupTexture(u64 hash) const;
|
|
||||||
void CacheTexture(u64 hash, const std::vector<u8>& tex, u32 width, u32 height);
|
|
||||||
|
|
||||||
void AddTexturePath(u64 hash, const std::string& path);
|
|
||||||
void FindCustomTextures(u64 program_id);
|
|
||||||
void PreloadTextures(Frontend::ImageInterface& image_interface);
|
|
||||||
bool CustomTextureExists(u64 hash) const;
|
|
||||||
const CustomTexPathInfo& LookupTexturePathInfo(u64 hash) const;
|
|
||||||
bool IsTexturePathMapEmpty() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unordered_set<u64> dumped_textures;
|
|
||||||
std::unordered_map<u64, CustomTexInfo> custom_textures;
|
|
||||||
std::unordered_map<u64, CustomTexPathInfo> custom_texture_paths;
|
|
||||||
};
|
|
||||||
} // namespace Core
|
|
@ -813,8 +813,7 @@ std::vector<FormatInfo> ListFormats() {
|
|||||||
void* data = nullptr; // For libavformat to save the iteration state
|
void* data = nullptr; // For libavformat to save the iteration state
|
||||||
while ((current = av_muxer_iterate(&data))) {
|
while ((current = av_muxer_iterate(&data))) {
|
||||||
#endif
|
#endif
|
||||||
std::vector<std::string> extensions;
|
const auto extensions = Common::SplitString(ToStdString(current->extensions), ',');
|
||||||
Common::SplitString(ToStdString(current->extensions), ',', extensions);
|
|
||||||
|
|
||||||
std::set<AVCodecID> supported_video_codecs;
|
std::set<AVCodecID> supported_video_codecs;
|
||||||
std::set<AVCodecID> supported_audio_codecs;
|
std::set<AVCodecID> supported_audio_codecs;
|
||||||
|
@ -32,7 +32,7 @@ PathParser::PathParser(const Path& path) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Common::SplitString(path_string, '/', path_sequence);
|
path_sequence = Common::SplitString(path_string, '/');
|
||||||
|
|
||||||
auto begin = path_sequence.begin();
|
auto begin = path_sequence.begin();
|
||||||
auto end = path_sequence.end();
|
auto end = path_sequence.end();
|
||||||
|
65
src/core/frontend/image_interface.cpp
Normal file
65
src/core/frontend/image_interface.cpp
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#define DDSKTX_IMPLEMENT
|
||||||
|
#include <dds-ktx.h>
|
||||||
|
#include <lodepng.h>
|
||||||
|
#include "common/file_util.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/frontend/image_interface.h"
|
||||||
|
|
||||||
|
namespace Frontend {
|
||||||
|
|
||||||
|
bool ImageInterface::DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
||||||
|
std::span<const u8> src) {
|
||||||
|
const u32 lodepng_ret = lodepng::decode(dst, width, height, src.data(), src.size());
|
||||||
|
if (lodepng_ret) {
|
||||||
|
LOG_ERROR(Frontend, "Failed to decode because {}", lodepng_error_text(lodepng_ret));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageInterface::DecodeDDS(std::vector<u8>& dst, u32& width, u32& height, ddsktx_format& format,
|
||||||
|
std::span<const u8> src) {
|
||||||
|
ddsktx_texture_info tc{};
|
||||||
|
const int size = static_cast<int>(src.size());
|
||||||
|
|
||||||
|
if (!ddsktx_parse(&tc, src.data(), size, nullptr)) {
|
||||||
|
LOG_ERROR(Frontend, "Failed to decode");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
width = tc.width;
|
||||||
|
height = tc.height;
|
||||||
|
format = tc.format;
|
||||||
|
|
||||||
|
ddsktx_sub_data sub_data{};
|
||||||
|
ddsktx_get_sub(&tc, &sub_data, src.data(), size, 0, 0, 0);
|
||||||
|
|
||||||
|
dst.resize(sub_data.size_bytes);
|
||||||
|
std::memcpy(dst.data(), sub_data.buff, sub_data.size_bytes);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageInterface::EncodePNG(const std::string& path, u32 width, u32 height,
|
||||||
|
std::span<const u8> src) {
|
||||||
|
std::vector<u8> out;
|
||||||
|
const u32 lodepng_ret = lodepng::encode(out, src.data(), width, height);
|
||||||
|
if (lodepng_ret) {
|
||||||
|
LOG_ERROR(Frontend, "Failed to encode {} because {}", path,
|
||||||
|
lodepng_error_text(lodepng_ret));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUtil::IOFile file{path, "wb"};
|
||||||
|
if (file.WriteBytes(out.data(), out.size()) != out.size()) {
|
||||||
|
LOG_ERROR(Frontend, "Failed to save encode to path {}", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Frontend
|
@ -4,21 +4,26 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <dds-ktx.h>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
||||||
namespace Frontend {
|
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 {
|
class ImageInterface {
|
||||||
public:
|
public:
|
||||||
virtual ~ImageInterface() = default;
|
virtual ~ImageInterface() = default;
|
||||||
|
|
||||||
// Error logging should be handled by the frontend
|
virtual bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height, std::span<const u8> src);
|
||||||
virtual bool DecodePNG(std::vector<u8>& dst, u32& width, u32& height,
|
virtual bool DecodeDDS(std::vector<u8>& dst, u32& width, u32& height, ddsktx_format& format,
|
||||||
const std::string& path) = 0;
|
std::span<const u8> src);
|
||||||
virtual bool EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
|
virtual bool EncodePNG(const std::string& path, u32 width, u32 height, std::span<const u8> src);
|
||||||
u32 height) = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Frontend
|
} // namespace Frontend
|
||||||
|
@ -136,8 +136,7 @@ void HandleHioReply(const u8* const command_buffer, const u32 command_length) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::string command_str{command_pos, command_buffer + command_length};
|
const std::string command_str{command_pos, command_buffer + command_length};
|
||||||
std::vector<std::string> command_parts;
|
const auto command_parts = Common::SplitString(command_str, ',');
|
||||||
Common::SplitString(command_str, ',', command_parts);
|
|
||||||
|
|
||||||
if (command_parts.empty() || command_parts.size() > 3) {
|
if (command_parts.empty() || command_parts.size() > 3) {
|
||||||
LOG_WARNING(Debug_GDBStub, "Unexpected reply packet size: {}", command_parts);
|
LOG_WARNING(Debug_GDBStub, "Unexpected reply packet size: {}", command_parts);
|
||||||
|
@ -458,8 +458,7 @@ void LoadPresetKeys() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> parts;
|
const auto parts = Common::SplitString(line, '=');
|
||||||
Common::SplitString(line, '=', parts);
|
|
||||||
if (parts.size() != 2) {
|
if (parts.size() != 2) {
|
||||||
LOG_ERROR(HW_AES, "Failed to parse {}", line);
|
LOG_ERROR(HW_AES, "Failed to parse {}", line);
|
||||||
continue;
|
continue;
|
||||||
|
@ -3,6 +3,12 @@ add_subdirectory(host_shaders)
|
|||||||
add_library(video_core STATIC
|
add_library(video_core STATIC
|
||||||
command_processor.cpp
|
command_processor.cpp
|
||||||
command_processor.h
|
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.cpp
|
||||||
debug_utils/debug_utils.h
|
debug_utils/debug_utils.h
|
||||||
geometry_pipeline.cpp
|
geometry_pipeline.cpp
|
||||||
@ -120,7 +126,7 @@ target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE})
|
|||||||
create_target_directory_groups(video_core)
|
create_target_directory_groups(video_core)
|
||||||
|
|
||||||
target_link_libraries(video_core PUBLIC common 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})
|
set_target_properties(video_core PROPERTIES INTERPROCEDURAL_OPTIMIZATION ${ENABLE_LTO})
|
||||||
|
|
||||||
if ("x86_64" IN_LIST ARCHITECTURE)
|
if ("x86_64" IN_LIST ARCHITECTURE)
|
||||||
|
36
src/video_core/custom_textures/custom_format.cpp
Normal file
36
src/video_core/custom_textures/custom_format.cpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "video_core/custom_textures/custom_format.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
std::string_view CustomPixelFormatAsString(CustomPixelFormat format) {
|
||||||
|
switch (format) {
|
||||||
|
case CustomPixelFormat::RGBA8:
|
||||||
|
return "RGBA8";
|
||||||
|
case CustomPixelFormat::BC1:
|
||||||
|
return "BC1";
|
||||||
|
case CustomPixelFormat::BC3:
|
||||||
|
return "BC3";
|
||||||
|
case CustomPixelFormat::BC5:
|
||||||
|
return "BC5";
|
||||||
|
case CustomPixelFormat::BC7:
|
||||||
|
return "BC7";
|
||||||
|
case CustomPixelFormat::ASTC4:
|
||||||
|
return "ASTC4";
|
||||||
|
case CustomPixelFormat::ASTC6:
|
||||||
|
return "ASTC6";
|
||||||
|
case CustomPixelFormat::ASTC8:
|
||||||
|
return "ASTC8";
|
||||||
|
default:
|
||||||
|
return "NotReal";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsCustomFormatCompressed(CustomPixelFormat format) {
|
||||||
|
return format != CustomPixelFormat::RGBA8 && format != CustomPixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
36
src/video_core/custom_textures/custom_format.h
Normal file
36
src/video_core/custom_textures/custom_format.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
#include <string_view>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
enum class CustomPixelFormat : u32 {
|
||||||
|
RGBA8 = 0,
|
||||||
|
BC1 = 1,
|
||||||
|
BC3 = 2,
|
||||||
|
BC5 = 3,
|
||||||
|
BC7 = 4,
|
||||||
|
ASTC4 = 5,
|
||||||
|
ASTC6 = 6,
|
||||||
|
ASTC8 = 7,
|
||||||
|
Invalid = std::numeric_limits<u32>::max(),
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CustomFileFormat : u32 {
|
||||||
|
None = 0,
|
||||||
|
PNG = 1,
|
||||||
|
DDS = 2,
|
||||||
|
KTX = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string_view CustomPixelFormatAsString(CustomPixelFormat format);
|
||||||
|
|
||||||
|
bool IsCustomFormatCompressed(CustomPixelFormat format);
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
352
src/video_core/custom_textures/custom_tex_manager.cpp
Normal file
352
src/video_core/custom_textures/custom_tex_manager.cpp
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <json.hpp>
|
||||||
|
#include "common/file_util.h"
|
||||||
|
#include "common/memory_detect.h"
|
||||||
|
#include "common/microprofile.h"
|
||||||
|
#include "common/settings.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "common/texture.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "core/frontend/image_interface.h"
|
||||||
|
#include "video_core/custom_textures/custom_tex_manager.h"
|
||||||
|
#include "video_core/rasterizer_cache/surface_params.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
MICROPROFILE_DEFINE(CustomTexManager_TickFrame, "CustomTexManager", "TickFrame",
|
||||||
|
MP_RGB(54, 16, 32));
|
||||||
|
|
||||||
|
constexpr std::size_t MAX_UPLOADS_PER_TICK = 16;
|
||||||
|
|
||||||
|
bool IsPow2(u32 value) {
|
||||||
|
return value != 0 && (value & (value - 1)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFileFormat MakeFileFormat(std::string_view ext) {
|
||||||
|
if (ext == "png") {
|
||||||
|
return CustomFileFormat::PNG;
|
||||||
|
} else if (ext == "dds") {
|
||||||
|
return CustomFileFormat::DDS;
|
||||||
|
} else if (ext == "ktx") {
|
||||||
|
return CustomFileFormat::KTX;
|
||||||
|
}
|
||||||
|
return CustomFileFormat::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
MapType MakeMapType(std::string_view ext) {
|
||||||
|
if (ext == "norm") {
|
||||||
|
return MapType::Normal;
|
||||||
|
}
|
||||||
|
LOG_ERROR(Render, "Unknown material extension {}", ext);
|
||||||
|
return MapType::Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
CustomTexManager::CustomTexManager(Core::System& system_)
|
||||||
|
: system{system_}, image_interface{*system.GetImageInterface()},
|
||||||
|
async_custom_loading{Settings::values.async_custom_loading.GetValue()} {}
|
||||||
|
|
||||||
|
CustomTexManager::~CustomTexManager() = default;
|
||||||
|
|
||||||
|
void CustomTexManager::TickFrame() {
|
||||||
|
MICROPROFILE_SCOPE(CustomTexManager_TickFrame);
|
||||||
|
if (!textures_loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::size_t num_uploads = 0;
|
||||||
|
for (auto it = async_uploads.begin(); it != async_uploads.end();) {
|
||||||
|
if (num_uploads >= MAX_UPLOADS_PER_TICK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (it->material->state) {
|
||||||
|
case DecodeState::Decoded:
|
||||||
|
it->func();
|
||||||
|
num_uploads++;
|
||||||
|
[[fallthrough]];
|
||||||
|
case DecodeState::Failed:
|
||||||
|
it = async_uploads.erase(it);
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
it++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomTexManager::FindCustomTextures() {
|
||||||
|
if (textures_loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!workers) {
|
||||||
|
CreateWorkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
const u64 program_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
|
||||||
|
const std::string load_path =
|
||||||
|
fmt::format("{}textures/{:016X}/", GetUserPath(FileUtil::UserPath::LoadDir), program_id);
|
||||||
|
|
||||||
|
if (!FileUtil::Exists(load_path)) {
|
||||||
|
FileUtil::CreateFullPath(load_path);
|
||||||
|
}
|
||||||
|
ReadConfig(load_path);
|
||||||
|
|
||||||
|
FileUtil::FSTEntry texture_dir;
|
||||||
|
std::vector<FileUtil::FSTEntry> textures;
|
||||||
|
FileUtil::ScanDirectoryTree(load_path, texture_dir, 64);
|
||||||
|
FileUtil::GetAllFilesFromNestedEntries(texture_dir, textures);
|
||||||
|
|
||||||
|
custom_textures.reserve(textures.size());
|
||||||
|
for (const FileUtil::FSTEntry& file : textures) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
custom_textures.push_back(std::make_unique<CustomTexture>(image_interface));
|
||||||
|
CustomTexture* const texture{custom_textures.back().get()};
|
||||||
|
if (!ParseFilename(file, texture)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto& material = material_map[texture->hash];
|
||||||
|
if (!material) {
|
||||||
|
material = std::make_unique<Material>();
|
||||||
|
}
|
||||||
|
material->AddMapTexture(texture);
|
||||||
|
}
|
||||||
|
textures_loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CustomTexManager::ParseFilename(const FileUtil::FSTEntry& file, CustomTexture* texture) {
|
||||||
|
auto parts = Common::SplitString(file.virtualName, '.');
|
||||||
|
if (parts.size() > 3) {
|
||||||
|
LOG_ERROR(Render, "Invalid filename {}, ignoring", file.virtualName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The last string should always be the file extension.
|
||||||
|
const CustomFileFormat file_format = MakeFileFormat(parts.back());
|
||||||
|
if (file_format == CustomFileFormat::None) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file_format == CustomFileFormat::DDS && refuse_dds) {
|
||||||
|
LOG_ERROR(Render, "Legacy pack is attempting to use DDS textures, skipping!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
texture->file_format = file_format;
|
||||||
|
parts.pop_back();
|
||||||
|
|
||||||
|
// This means the texture is a material type other than color.
|
||||||
|
texture->type = MapType::Color;
|
||||||
|
if (parts.size() > 1) {
|
||||||
|
texture->type = MakeMapType(parts.back());
|
||||||
|
parts.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if the path is mapped directly to a hash
|
||||||
|
// before trying to parse the texture filename.
|
||||||
|
const auto it = path_to_hash_map.find(file.virtualName);
|
||||||
|
if (it != path_to_hash_map.end()) {
|
||||||
|
texture->hash = it->second;
|
||||||
|
} else {
|
||||||
|
u32 width;
|
||||||
|
u32 height;
|
||||||
|
u32 format;
|
||||||
|
unsigned long long hash{};
|
||||||
|
if (std::sscanf(parts.back().c_str(), "tex1_%ux%u_%llX_%u", &width, &height, &hash,
|
||||||
|
&format) != 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
texture->hash = hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
texture->path = file.physicalName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomTexManager::WriteConfig() {
|
||||||
|
const u64 program_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
|
||||||
|
const std::string dump_path =
|
||||||
|
fmt::format("{}textures/{:016X}/", GetUserPath(FileUtil::UserPath::DumpDir), program_id);
|
||||||
|
const std::string pack_config = dump_path + "pack.json";
|
||||||
|
if (FileUtil::Exists(pack_config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::ordered_json json;
|
||||||
|
json["author"] = "citra";
|
||||||
|
json["version"] = "1.0.0";
|
||||||
|
json["description"] = "A graphics pack";
|
||||||
|
|
||||||
|
auto& options = json["options"];
|
||||||
|
options["skip_mipmap"] = skip_mipmap;
|
||||||
|
options["flip_png_files"] = flip_png_files;
|
||||||
|
options["use_new_hash"] = use_new_hash;
|
||||||
|
|
||||||
|
FileUtil::IOFile file{pack_config, "w"};
|
||||||
|
const std::string output = json.dump(4);
|
||||||
|
file.WriteString(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomTexManager::PreloadTextures() {
|
||||||
|
u64 size_sum = 0;
|
||||||
|
const u64 sys_mem = Common::GetMemInfo().total_physical_memory;
|
||||||
|
const u64 recommended_min_mem = 2 * size_t(1024 * 1024 * 1024);
|
||||||
|
|
||||||
|
// keep 2GB memory for system stability if system RAM is 4GB+ - use half of memory in other
|
||||||
|
// cases
|
||||||
|
const u64 max_mem =
|
||||||
|
(sys_mem / 2 < recommended_min_mem) ? (sys_mem / 2) : (sys_mem - recommended_min_mem);
|
||||||
|
|
||||||
|
workers->QueueWork([&]() {
|
||||||
|
for (auto& [hash, material] : material_map) {
|
||||||
|
if (size_sum > max_mem) {
|
||||||
|
LOG_WARNING(Render, "Aborting texture preload due to insufficient memory");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
material->LoadFromDisk(flip_png_files);
|
||||||
|
size_sum += material->size;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
workers->WaitForRequests();
|
||||||
|
async_custom_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomTexManager::DumpTexture(const SurfaceParams& params, u32 level, std::span<u8> data,
|
||||||
|
u64 data_hash) {
|
||||||
|
const u64 program_id = system.Kernel().GetCurrentProcess()->codeset->program_id;
|
||||||
|
const u32 data_size = static_cast<u32>(data.size());
|
||||||
|
const u32 width = params.width;
|
||||||
|
const u32 height = params.height;
|
||||||
|
const PixelFormat format = params.pixel_format;
|
||||||
|
|
||||||
|
std::string dump_path = fmt::format(
|
||||||
|
"{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id);
|
||||||
|
if (!FileUtil::CreateFullPath(dump_path)) {
|
||||||
|
LOG_ERROR(Render, "Unable to create {}", dump_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_path +=
|
||||||
|
fmt::format("tex1_{}x{}_{:016X}_{}_mip{}.png", width, height, data_hash, format, level);
|
||||||
|
if (dumped_textures.contains(data_hash) || FileUtil::Exists(dump_path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the texture size is a power of 2.
|
||||||
|
// If not, the surface is probably a framebuffer
|
||||||
|
if (!IsPow2(width) || !IsPow2(height)) {
|
||||||
|
LOG_WARNING(Render, "Not dumping {:016X} because size isn't a power of 2 ({}x{})",
|
||||||
|
data_hash, width, height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 decoded_size = width * height * 4;
|
||||||
|
std::vector<u8> pixels(data_size + decoded_size);
|
||||||
|
std::memcpy(pixels.data(), data.data(), data_size);
|
||||||
|
|
||||||
|
auto dump = [this, width, height, params, data_size, decoded_size, pixels = std::move(pixels),
|
||||||
|
dump_path = std::move(dump_path)]() mutable {
|
||||||
|
const std::span encoded = std::span{pixels}.first(data_size);
|
||||||
|
const std::span decoded = std::span{pixels}.last(decoded_size);
|
||||||
|
DecodeTexture(params, params.addr, params.end, encoded, decoded,
|
||||||
|
params.type == SurfaceType::Color);
|
||||||
|
Common::FlipRGBA8Texture(decoded, width, height);
|
||||||
|
image_interface.EncodePNG(dump_path, width, height, decoded);
|
||||||
|
};
|
||||||
|
if (!workers) {
|
||||||
|
CreateWorkers();
|
||||||
|
}
|
||||||
|
workers->QueueWork(std::move(dump));
|
||||||
|
dumped_textures.insert(data_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
Material* CustomTexManager::GetMaterial(u64 data_hash) {
|
||||||
|
const auto it = material_map.find(data_hash);
|
||||||
|
if (it == material_map.end()) {
|
||||||
|
LOG_WARNING(Render, "Unable to find replacement for surface with hash {:016X}", data_hash);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CustomTexManager::Decode(Material* material, std::function<bool()>&& upload) {
|
||||||
|
if (!async_custom_loading) {
|
||||||
|
material->LoadFromDisk(flip_png_files);
|
||||||
|
return upload();
|
||||||
|
}
|
||||||
|
if (material->IsUnloaded()) {
|
||||||
|
material->state = DecodeState::Pending;
|
||||||
|
workers->QueueWork([material, this] { material->LoadFromDisk(flip_png_files); });
|
||||||
|
}
|
||||||
|
async_uploads.push_back({
|
||||||
|
.material = material,
|
||||||
|
.func = std::move(upload),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomTexManager::ReadConfig(const std::string& load_path) {
|
||||||
|
const std::string config_path = load_path + "pack.json";
|
||||||
|
FileUtil::IOFile file{config_path, "r"};
|
||||||
|
if (!file.IsOpen()) {
|
||||||
|
LOG_INFO(Render, "Unable to find pack config file, using legacy defaults");
|
||||||
|
refuse_dds = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string config(file.GetSize(), '\0');
|
||||||
|
const std::size_t read_size = file.ReadBytes(config.data(), config.size());
|
||||||
|
if (!read_size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json json = nlohmann::json::parse(config);
|
||||||
|
|
||||||
|
const auto& options = json["options"];
|
||||||
|
skip_mipmap = options["skip_mipmap"].get<bool>();
|
||||||
|
flip_png_files = options["flip_png_files"].get<bool>();
|
||||||
|
use_new_hash = options["use_new_hash"].get<bool>();
|
||||||
|
refuse_dds = skip_mipmap || !use_new_hash;
|
||||||
|
|
||||||
|
const auto& textures = json["textures"];
|
||||||
|
for (const auto& material : textures.items()) {
|
||||||
|
size_t idx{};
|
||||||
|
const u64 hash = std::stoull(material.key(), &idx, 16);
|
||||||
|
if (!idx) {
|
||||||
|
LOG_ERROR(Render, "Key {} is invalid, skipping", material.key());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto parse = [&](const std::string& file) {
|
||||||
|
const std::string filename{FileUtil::GetFilename(file)};
|
||||||
|
auto [it, new_hash] = path_to_hash_map.try_emplace(filename);
|
||||||
|
if (!new_hash) {
|
||||||
|
LOG_ERROR(Render,
|
||||||
|
"File {} with key {} already exists and is mapped to {:#016X}, skipping",
|
||||||
|
file, material.key(), path_to_hash_map[filename]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
it->second = hash;
|
||||||
|
};
|
||||||
|
const auto value = material.value();
|
||||||
|
if (value.is_string()) {
|
||||||
|
const auto file = value.get<std::string>();
|
||||||
|
parse(file);
|
||||||
|
} else if (value.is_array()) {
|
||||||
|
const auto files = value.get<std::vector<std::string>>();
|
||||||
|
for (const std::string& file : files) {
|
||||||
|
parse(file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Render, "Material with key {} is invalid", material.key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomTexManager::CreateWorkers() {
|
||||||
|
const std::size_t num_workers = std::max(std::thread::hardware_concurrency(), 2U) - 1;
|
||||||
|
workers = std::make_unique<Common::ThreadWorker>(num_workers, "Custom textures");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
94
src/video_core/custom_textures/custom_tex_manager.h
Normal file
94
src/video_core/custom_textures/custom_tex_manager.h
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <list>
|
||||||
|
#include <span>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include "common/thread_worker.h"
|
||||||
|
#include "video_core/custom_textures/material.h"
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class System;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace FileUtil {
|
||||||
|
struct FSTEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
class SurfaceParams;
|
||||||
|
|
||||||
|
struct AsyncUpload {
|
||||||
|
const Material* material;
|
||||||
|
std::function<bool()> func;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CustomTexManager {
|
||||||
|
public:
|
||||||
|
explicit CustomTexManager(Core::System& system);
|
||||||
|
~CustomTexManager();
|
||||||
|
|
||||||
|
/// Processes queued texture uploads
|
||||||
|
void TickFrame();
|
||||||
|
|
||||||
|
/// Searches the load directory assigned to program_id for any custom textures and loads them
|
||||||
|
void FindCustomTextures();
|
||||||
|
|
||||||
|
/// Saves the pack configuration file template to the dump directory if it doesn't exist.
|
||||||
|
void WriteConfig();
|
||||||
|
|
||||||
|
/// Preloads all registered custom textures
|
||||||
|
void PreloadTextures();
|
||||||
|
|
||||||
|
/// Saves the provided pixel data described by params to disk as png
|
||||||
|
void DumpTexture(const SurfaceParams& params, u32 level, std::span<u8> data, u64 data_hash);
|
||||||
|
|
||||||
|
/// Returns the material assigned to the provided data hash
|
||||||
|
Material* GetMaterial(u64 data_hash);
|
||||||
|
|
||||||
|
/// Decodes the textures in material to a consumable format and uploads it.
|
||||||
|
bool Decode(Material* material, std::function<bool()>&& upload);
|
||||||
|
|
||||||
|
/// True when mipmap uploads should be skipped (legacy packs only)
|
||||||
|
bool SkipMipmaps() const noexcept {
|
||||||
|
return skip_mipmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the pack uses the new hashing method.
|
||||||
|
bool UseNewHash() const noexcept {
|
||||||
|
return use_new_hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// Parses the custom texture filename (hash, material type, etc).
|
||||||
|
bool ParseFilename(const FileUtil::FSTEntry& file, CustomTexture* texture);
|
||||||
|
|
||||||
|
/// Reads the pack configuration file
|
||||||
|
void ReadConfig(const std::string& load_path);
|
||||||
|
|
||||||
|
/// Creates the thread workers.
|
||||||
|
void CreateWorkers();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Core::System& system;
|
||||||
|
Frontend::ImageInterface& image_interface;
|
||||||
|
std::unordered_set<u64> dumped_textures;
|
||||||
|
std::unordered_map<u64, std::unique_ptr<Material>> material_map;
|
||||||
|
std::unordered_map<std::string, u64> path_to_hash_map;
|
||||||
|
std::vector<std::unique_ptr<CustomTexture>> custom_textures;
|
||||||
|
std::list<AsyncUpload> async_uploads;
|
||||||
|
std::unique_ptr<Common::ThreadWorker> workers;
|
||||||
|
bool textures_loaded{false};
|
||||||
|
bool async_custom_loading{true};
|
||||||
|
bool skip_mipmap{true};
|
||||||
|
bool flip_png_files{true};
|
||||||
|
bool use_new_hash{false};
|
||||||
|
bool refuse_dds{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
151
src/video_core/custom_textures/material.cpp
Normal file
151
src/video_core/custom_textures/material.cpp
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/file_util.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "common/texture.h"
|
||||||
|
#include "core/frontend/image_interface.h"
|
||||||
|
#include "video_core/custom_textures/material.h"
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
CustomPixelFormat ToCustomPixelFormat(ddsktx_format format) {
|
||||||
|
switch (format) {
|
||||||
|
case DDSKTX_FORMAT_RGBA8:
|
||||||
|
return CustomPixelFormat::RGBA8;
|
||||||
|
case DDSKTX_FORMAT_BC1:
|
||||||
|
return CustomPixelFormat::BC1;
|
||||||
|
case DDSKTX_FORMAT_BC3:
|
||||||
|
return CustomPixelFormat::BC3;
|
||||||
|
case DDSKTX_FORMAT_BC5:
|
||||||
|
return CustomPixelFormat::BC5;
|
||||||
|
case DDSKTX_FORMAT_BC7:
|
||||||
|
return CustomPixelFormat::BC7;
|
||||||
|
case DDSKTX_FORMAT_ASTC4x4:
|
||||||
|
return CustomPixelFormat::ASTC4;
|
||||||
|
case DDSKTX_FORMAT_ASTC6x6:
|
||||||
|
return CustomPixelFormat::ASTC6;
|
||||||
|
case DDSKTX_FORMAT_ASTC8x6:
|
||||||
|
return CustomPixelFormat::ASTC8;
|
||||||
|
default:
|
||||||
|
LOG_ERROR(Common, "Unknown dds/ktx pixel format {}", format);
|
||||||
|
return CustomPixelFormat::RGBA8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view MapTypeName(MapType type) {
|
||||||
|
switch (type) {
|
||||||
|
case MapType::Color:
|
||||||
|
return "Color";
|
||||||
|
case MapType::Normal:
|
||||||
|
return "Normal";
|
||||||
|
default:
|
||||||
|
return "Invalid";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Anonymous namespace
|
||||||
|
|
||||||
|
CustomTexture::CustomTexture(Frontend::ImageInterface& image_interface_)
|
||||||
|
: image_interface{image_interface_} {}
|
||||||
|
|
||||||
|
CustomTexture::~CustomTexture() = default;
|
||||||
|
|
||||||
|
void CustomTexture::LoadFromDisk(bool flip_png) {
|
||||||
|
FileUtil::IOFile file{path, "rb"};
|
||||||
|
std::vector<u8> input(file.GetSize());
|
||||||
|
if (file.ReadBytes(input.data(), input.size()) != input.size()) {
|
||||||
|
LOG_CRITICAL(Render, "Failed to open custom texture: {}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (file_format) {
|
||||||
|
case CustomFileFormat::PNG:
|
||||||
|
LoadPNG(input, flip_png);
|
||||||
|
break;
|
||||||
|
case CustomFileFormat::DDS:
|
||||||
|
case CustomFileFormat::KTX:
|
||||||
|
LoadDDS(input);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG_ERROR(Render, "Unknown file format {}", file_format);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomTexture::LoadPNG(std::span<const u8> input, bool flip_png) {
|
||||||
|
if (!image_interface.DecodePNG(data, width, height, input)) {
|
||||||
|
LOG_ERROR(Render, "Failed to decode png: {}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (flip_png) {
|
||||||
|
Common::FlipRGBA8Texture(data, width, height);
|
||||||
|
}
|
||||||
|
format = CustomPixelFormat::RGBA8;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomTexture::LoadDDS(std::span<const u8> input) {
|
||||||
|
ddsktx_format dds_format{};
|
||||||
|
image_interface.DecodeDDS(data, width, height, dds_format, input);
|
||||||
|
format = ToCustomPixelFormat(dds_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::LoadFromDisk(bool flip_png) noexcept {
|
||||||
|
if (IsDecoded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (CustomTexture* const texture : textures) {
|
||||||
|
if (!texture || texture->IsLoaded()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
texture->LoadFromDisk(flip_png);
|
||||||
|
size += texture->data.size();
|
||||||
|
LOG_DEBUG(Render, "Loading {} map {} with hash {:#016X}", MapTypeName(texture->type),
|
||||||
|
texture->path, texture->hash);
|
||||||
|
}
|
||||||
|
if (!textures[0]) {
|
||||||
|
LOG_ERROR(Render, "Unable to create material without color texture!");
|
||||||
|
state = DecodeState::Failed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
width = textures[0]->width;
|
||||||
|
height = textures[0]->height;
|
||||||
|
format = textures[0]->format;
|
||||||
|
for (const CustomTexture* texture : textures) {
|
||||||
|
if (!texture) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (texture->width != width || texture->height != height) {
|
||||||
|
LOG_ERROR(Render,
|
||||||
|
"{} map {} of material with hash {:#016X} has dimentions {}x{} "
|
||||||
|
"which do not match the color texture dimentions {}x{}",
|
||||||
|
MapTypeName(texture->type), texture->path, texture->hash, texture->width,
|
||||||
|
texture->height, width, height);
|
||||||
|
state = DecodeState::Failed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (texture->format != format) {
|
||||||
|
LOG_ERROR(
|
||||||
|
Render, "{} map {} is stored with {} format which does not match color format {}",
|
||||||
|
MapTypeName(texture->type), texture->path,
|
||||||
|
CustomPixelFormatAsString(texture->format), CustomPixelFormatAsString(format));
|
||||||
|
state = DecodeState::Failed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = DecodeState::Decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Material::AddMapTexture(CustomTexture* texture) noexcept {
|
||||||
|
const std::size_t index = static_cast<std::size_t>(texture->type);
|
||||||
|
if (textures[index]) {
|
||||||
|
LOG_ERROR(Render, "Textures {} and {} are assigned to the same material, ignoring!",
|
||||||
|
textures[index]->path, texture->path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
textures[index] = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
99
src/video_core/custom_textures/material.h
Normal file
99
src/video_core/custom_textures/material.h
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// Copyright 2023 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "video_core/custom_textures/custom_format.h"
|
||||||
|
|
||||||
|
namespace Frontend {
|
||||||
|
class ImageInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
|
||||||
|
enum class MapType : u32 {
|
||||||
|
Color = 0,
|
||||||
|
Normal = 1,
|
||||||
|
MapCount = 2,
|
||||||
|
};
|
||||||
|
constexpr std::size_t MAX_MAPS = static_cast<std::size_t>(MapType::MapCount);
|
||||||
|
|
||||||
|
enum class DecodeState : u32 {
|
||||||
|
None = 0,
|
||||||
|
Pending = 1,
|
||||||
|
Decoded = 2,
|
||||||
|
Failed = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
class CustomTexture {
|
||||||
|
public:
|
||||||
|
explicit CustomTexture(Frontend::ImageInterface& image_interface);
|
||||||
|
~CustomTexture();
|
||||||
|
|
||||||
|
void LoadFromDisk(bool flip_png);
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsParsed() const noexcept {
|
||||||
|
return file_format != CustomFileFormat::None && hash != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsLoaded() const noexcept {
|
||||||
|
return !data.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void LoadPNG(std::span<const u8> input, bool flip_png);
|
||||||
|
|
||||||
|
void LoadDDS(std::span<const u8> input);
|
||||||
|
|
||||||
|
public:
|
||||||
|
Frontend::ImageInterface& image_interface;
|
||||||
|
std::string path;
|
||||||
|
u32 width;
|
||||||
|
u32 height;
|
||||||
|
u64 hash;
|
||||||
|
CustomPixelFormat format;
|
||||||
|
CustomFileFormat file_format;
|
||||||
|
std::vector<u8> data;
|
||||||
|
MapType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Material {
|
||||||
|
u32 width;
|
||||||
|
u32 height;
|
||||||
|
u64 size;
|
||||||
|
CustomPixelFormat format;
|
||||||
|
std::array<CustomTexture*, MAX_MAPS> textures;
|
||||||
|
std::atomic<DecodeState> state{};
|
||||||
|
|
||||||
|
void LoadFromDisk(bool flip_png) noexcept;
|
||||||
|
|
||||||
|
void AddMapTexture(CustomTexture* texture) noexcept;
|
||||||
|
|
||||||
|
[[nodiscard]] CustomTexture* Map(MapType type) const noexcept {
|
||||||
|
return textures.at(static_cast<std::size_t>(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsPending() const noexcept {
|
||||||
|
return state == DecodeState::Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsFailed() const noexcept {
|
||||||
|
return state == DecodeState::Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsDecoded() const noexcept {
|
||||||
|
return state == DecodeState::Decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsUnloaded() const noexcept {
|
||||||
|
return state == DecodeState::None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace VideoCore
|
@ -44,7 +44,7 @@ enum class SurfaceType : u32 {
|
|||||||
Invalid = 5,
|
Invalid = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TextureType : u32 {
|
enum class TextureType : u16 {
|
||||||
Texture2D = 0,
|
Texture2D = 0,
|
||||||
CubeMap = 1,
|
CubeMap = 1,
|
||||||
};
|
};
|
||||||
@ -99,10 +99,10 @@ constexpr SurfaceType GetFormatType(PixelFormat format) {
|
|||||||
return FORMAT_MAP[index].type;
|
return FORMAT_MAP[index].type;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string_view PixelFormatAsString(PixelFormat format);
|
|
||||||
|
|
||||||
bool CheckFormatsBlittable(PixelFormat source_format, PixelFormat dest_format);
|
bool CheckFormatsBlittable(PixelFormat source_format, PixelFormat dest_format);
|
||||||
|
|
||||||
|
std::string_view PixelFormatAsString(PixelFormat format);
|
||||||
|
|
||||||
PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format);
|
PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format);
|
||||||
|
|
||||||
PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format);
|
PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format);
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/microprofile.h"
|
#include "common/microprofile.h"
|
||||||
#include "core/memory.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/rasterizer_cache/rasterizer_cache.h"
|
||||||
#include "video_core/regs.h"
|
#include "video_core/regs.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
@ -19,6 +20,15 @@ namespace {
|
|||||||
|
|
||||||
MICROPROFILE_DEFINE(RasterizerCache_CopySurface, "RasterizerCache", "CopySurface",
|
MICROPROFILE_DEFINE(RasterizerCache_CopySurface, "RasterizerCache", "CopySurface",
|
||||||
MP_RGB(128, 192, 64));
|
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) {
|
constexpr auto RangeFromInterval(const auto& map, const auto& interval) {
|
||||||
return boost::make_iterator_range(map.equal_range(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
|
} // Anonymous namespace
|
||||||
|
|
||||||
RasterizerCache::RasterizerCache(Memory::MemorySystem& memory_, OpenGL::TextureRuntime& runtime_,
|
RasterizerCache::RasterizerCache(Memory::MemorySystem& memory_,
|
||||||
Pica::Regs& regs_, RendererBase& renderer_)
|
CustomTexManager& custom_tex_manager_,
|
||||||
: memory{memory_}, runtime{runtime_}, regs{regs_}, renderer{renderer_},
|
OpenGL::TextureRuntime& runtime_, Pica::Regs& regs_,
|
||||||
resolution_scale_factor{renderer.GetResolutionScaleFactor()},
|
RendererBase& renderer_)
|
||||||
use_filter{Settings::values.texture_filter.GetValue() != Settings::TextureFilter::None} {}
|
: 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() {
|
RasterizerCache::~RasterizerCache() {
|
||||||
#ifndef ANDROID
|
#ifndef ANDROID
|
||||||
@ -132,6 +146,32 @@ RasterizerCache::~RasterizerCache() {
|
|||||||
#endif
|
#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) {
|
bool RasterizerCache::AccelerateTextureCopy(const GPU::Regs::DisplayTransferConfig& config) {
|
||||||
// Texture copy size is aligned to 16 byte units
|
// Texture copy size is aligned to 16 byte units
|
||||||
const u32 copy_size = Common::AlignDown(config.texture_copy.size, 16);
|
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) {
|
bool using_depth_fb) {
|
||||||
const auto& config = regs.framebuffer.framebuffer;
|
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_width = config.GetWidth();
|
||||||
const s32 framebuffer_height = config.GetHeight();
|
const s32 framebuffer_height = config.GetHeight();
|
||||||
const auto viewport_rect = regs.rasterizer.GetViewportRect();
|
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
|
// Filtered mipmaps often look really bad. We can achieve better quality by
|
||||||
// generating them from the base level.
|
// generating them from the base level.
|
||||||
if (surface->res_scale != 1 && level != 0) {
|
if (surface->res_scale != 1 && level != 0) {
|
||||||
runtime.GenerateMipmaps(*surface, surface->levels - 1);
|
runtime.GenerateMipmaps(*surface);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RasterizerCache::UploadSurface(const SurfaceRef& surface, SurfaceInterval interval) {
|
void RasterizerCache::UploadSurface(const SurfaceRef& surface, SurfaceInterval interval) {
|
||||||
|
MICROPROFILE_SCOPE(RasterizerCache_UploadSurface);
|
||||||
|
|
||||||
const SurfaceParams load_info = surface->FromInterval(interval);
|
const SurfaceParams load_info = surface->FromInterval(interval);
|
||||||
ASSERT(load_info.addr >= surface->addr && load_info.end <= surface->end);
|
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,
|
DecodeTexture(load_info, load_info.addr, load_info.end, upload_data, staging.mapped,
|
||||||
runtime.NeedsConversion(surface->pixel_format));
|
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 = {
|
const BufferTextureCopy upload = {
|
||||||
.buffer_offset = 0,
|
.buffer_offset = 0,
|
||||||
.buffer_size = staging.size,
|
.buffer_size = staging.size,
|
||||||
@ -847,7 +886,49 @@ void RasterizerCache::UploadSurface(const SurfaceRef& surface, SurfaceInterval i
|
|||||||
surface->Upload(upload, staging);
|
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<u8> decoded,
|
||||||
|
std::span<u8> 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) {
|
void RasterizerCache::DownloadSurface(const SurfaceRef& surface, SurfaceInterval interval) {
|
||||||
|
MICROPROFILE_SCOPE(RasterizerCache_DownloadSurface);
|
||||||
|
|
||||||
const SurfaceParams flush_info = surface->FromInterval(interval);
|
const SurfaceParams flush_info = surface->FromInterval(interval);
|
||||||
const u32 flush_start = boost::icl::first(interval);
|
const u32 flush_start = boost::icl::first(interval);
|
||||||
const u32 flush_end = boost::icl::last_next(interval);
|
const u32 flush_end = boost::icl::last_next(interval);
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
#include <boost/icl/interval_map.hpp>
|
#include <boost/icl/interval_map.hpp>
|
||||||
#include <boost/icl/interval_set.hpp>
|
#include <boost/icl/interval_set.hpp>
|
||||||
#include "video_core/rasterizer_cache/surface_base.h"
|
#include "video_core/rasterizer_cache/surface_base.h"
|
||||||
@ -28,6 +30,8 @@ enum class ScaleMatch {
|
|||||||
Ignore // accept every scaled res
|
Ignore // accept every scaled res
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class CustomTexManager;
|
||||||
|
struct CustomTexture;
|
||||||
class RendererBase;
|
class RendererBase;
|
||||||
|
|
||||||
class RasterizerCache : NonCopyable {
|
class RasterizerCache : NonCopyable {
|
||||||
@ -62,10 +66,13 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
RasterizerCache(Memory::MemorySystem& memory, OpenGL::TextureRuntime& runtime, Pica::Regs& regs,
|
RasterizerCache(Memory::MemorySystem& memory, CustomTexManager& custom_tex_manager,
|
||||||
RendererBase& renderer);
|
OpenGL::TextureRuntime& runtime, Pica::Regs& regs, RendererBase& renderer);
|
||||||
~RasterizerCache();
|
~RasterizerCache();
|
||||||
|
|
||||||
|
/// Notify the cache that a new frame has been queued
|
||||||
|
void TickFrame();
|
||||||
|
|
||||||
/// Perform hardware accelerated texture copy according to the provided configuration
|
/// Perform hardware accelerated texture copy according to the provided configuration
|
||||||
bool AccelerateTextureCopy(const GPU::Regs::DisplayTransferConfig& config);
|
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
|
/// Copies pixel data in interval from the guest VRAM to the host GPU surface
|
||||||
void UploadSurface(const SurfaceRef& surface, SurfaceInterval interval);
|
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<u8> decoded,
|
||||||
|
std::span<u8> upload_data);
|
||||||
|
|
||||||
/// Copies pixel data in interval from the host GPU surface to the guest VRAM
|
/// Copies pixel data in interval from the host GPU surface to the guest VRAM
|
||||||
void DownloadSurface(const SurfaceRef& surface, SurfaceInterval interval);
|
void DownloadSurface(const SurfaceRef& surface, SurfaceInterval interval);
|
||||||
|
|
||||||
@ -158,6 +172,7 @@ private:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Memory::MemorySystem& memory;
|
Memory::MemorySystem& memory;
|
||||||
|
CustomTexManager& custom_tex_manager;
|
||||||
OpenGL::TextureRuntime& runtime;
|
OpenGL::TextureRuntime& runtime;
|
||||||
Pica::Regs& regs;
|
Pica::Regs& regs;
|
||||||
RendererBase& renderer;
|
RendererBase& renderer;
|
||||||
@ -168,7 +183,9 @@ private:
|
|||||||
u32 resolution_scale_factor;
|
u32 resolution_scale_factor;
|
||||||
RenderTargets render_targets;
|
RenderTargets render_targets;
|
||||||
std::unordered_map<TextureCubeConfig, TextureCube> texture_cube_cache;
|
std::unordered_map<TextureCubeConfig, TextureCube> texture_cube_cache;
|
||||||
bool use_filter{};
|
bool use_filter;
|
||||||
|
bool dump_textures;
|
||||||
|
bool use_custom_textures;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace VideoCore
|
} // namespace VideoCore
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "common/alignment.h"
|
#include "common/alignment.h"
|
||||||
|
#include "video_core/custom_textures/material.h"
|
||||||
#include "video_core/rasterizer_cache/surface_base.h"
|
#include "video_core/rasterizer_cache/surface_base.h"
|
||||||
#include "video_core/texture/texture_decode.h"
|
#include "video_core/texture/texture_decode.h"
|
||||||
|
|
||||||
@ -101,6 +102,10 @@ SurfaceInterval SurfaceBase::GetCopyableInterval(const SurfaceParams& params) co
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SurfaceBase::HasNormalMap() const noexcept {
|
||||||
|
return material && material->Map(MapType::Normal) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
ClearValue SurfaceBase::MakeClearValue(PAddr copy_addr, PixelFormat dst_format) {
|
ClearValue SurfaceBase::MakeClearValue(PAddr copy_addr, PixelFormat dst_format) {
|
||||||
const SurfaceType dst_type = GetFormatType(dst_format);
|
const SurfaceType dst_type = GetFormatType(dst_format);
|
||||||
const std::array fill_buffer = MakeFillBuffer(copy_addr);
|
const std::array fill_buffer = MakeFillBuffer(copy_addr);
|
||||||
|
@ -11,6 +11,8 @@ namespace VideoCore {
|
|||||||
|
|
||||||
using SurfaceRegions = boost::icl::interval_set<PAddr, std::less, SurfaceInterval>;
|
using SurfaceRegions = boost::icl::interval_set<PAddr, std::less, SurfaceInterval>;
|
||||||
|
|
||||||
|
struct Material;
|
||||||
|
|
||||||
class SurfaceBase : public SurfaceParams {
|
class SurfaceBase : public SurfaceParams {
|
||||||
public:
|
public:
|
||||||
SurfaceBase(const SurfaceParams& params);
|
SurfaceBase(const SurfaceParams& params);
|
||||||
@ -28,10 +30,17 @@ public:
|
|||||||
/// Returns the clear value used to validate another surface from this fill surface
|
/// Returns the clear value used to validate another surface from this fill surface
|
||||||
ClearValue MakeClearValue(PAddr copy_addr, PixelFormat dst_format);
|
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 {
|
u64 ModificationTick() const noexcept {
|
||||||
return modification_tick;
|
return modification_tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsCustom() const noexcept {
|
||||||
|
return is_custom && custom_format != CustomPixelFormat::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
bool IsRegionValid(SurfaceInterval interval) const {
|
bool IsRegionValid(SurfaceInterval interval) const {
|
||||||
return (invalid_regions.find(interval) == invalid_regions.end());
|
return (invalid_regions.find(interval) == invalid_regions.end());
|
||||||
}
|
}
|
||||||
@ -57,6 +66,8 @@ private:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
bool registered = false;
|
bool registered = false;
|
||||||
|
bool is_custom = false;
|
||||||
|
const Material* material = nullptr;
|
||||||
SurfaceRegions invalid_regions;
|
SurfaceRegions invalid_regions;
|
||||||
u32 fill_size = 0;
|
u32 fill_size = 0;
|
||||||
std::array<u8, 4> fill_data;
|
std::array<u8, 4> fill_data;
|
||||||
|
@ -215,12 +215,12 @@ u32 SurfaceParams::LevelOf(PAddr level_addr) const {
|
|||||||
return level;
|
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_width = scaled ? GetScaledWidth() : width;
|
||||||
const u32 scaled_height = scaled ? GetScaledHeight() : height;
|
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_height, PixelFormatAsString(pixel_format), levels, addr, end,
|
||||||
scaled ? "scaled" : "unscaled");
|
custom ? "custom," : "", scaled ? "scaled" : "unscaled");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace VideoCore
|
} // namespace VideoCore
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "video_core/custom_textures/custom_format.h"
|
||||||
#include "video_core/rasterizer_cache/utils.h"
|
#include "video_core/rasterizer_cache/utils.h"
|
||||||
|
|
||||||
namespace VideoCore {
|
namespace VideoCore {
|
||||||
@ -46,7 +47,7 @@ public:
|
|||||||
u32 LevelOf(PAddr addr) const;
|
u32 LevelOf(PAddr addr) const;
|
||||||
|
|
||||||
/// Returns a string identifier of the params object
|
/// 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 {
|
[[nodiscard]] SurfaceInterval GetInterval() const noexcept {
|
||||||
return SurfaceInterval{addr, end};
|
return SurfaceInterval{addr, end};
|
||||||
@ -101,6 +102,7 @@ public:
|
|||||||
bool is_tiled = false;
|
bool is_tiled = false;
|
||||||
TextureType texture_type = TextureType::Texture2D;
|
TextureType texture_type = TextureType::Texture2D;
|
||||||
PixelFormat pixel_format = PixelFormat::Invalid;
|
PixelFormat pixel_format = PixelFormat::Invalid;
|
||||||
|
CustomPixelFormat custom_format = CustomPixelFormat::Invalid;
|
||||||
SurfaceType type = SurfaceType::Invalid;
|
SurfaceType type = SurfaceType::Invalid;
|
||||||
|
|
||||||
std::array<u32, MAX_PICA_LEVELS> mipmap_offsets{};
|
std::array<u32, MAX_PICA_LEVELS> mipmap_offsets{};
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
#include "video_core/host_shaders/texture_filtering/nearest_neighbor_frag.h"
|
#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/refine_frag.h"
|
||||||
#include "video_core/host_shaders/texture_filtering/scale_force_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/x_gradient_frag.h"
|
||||||
#include "video_core/host_shaders/texture_filtering/xbrz_freescale_frag.h"
|
#include "video_core/host_shaders/texture_filtering/xbrz_freescale_frag.h"
|
||||||
#include "video_core/host_shaders/texture_filtering/y_gradient_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();
|
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()};
|
const auto filter{Settings::values.texture_filter.GetValue()};
|
||||||
switch (filter) {
|
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());
|
auto LUMAD = setup_temp_tex(GL_R16F, GL_RED, temp_rect.GetWidth(), temp_rect.GetHeight());
|
||||||
|
|
||||||
// Copy to SRC
|
// 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,
|
blit.src_rect.bottom, 0, SRC.tex.handle, GL_TEXTURE_2D, 0, 0, 0, 0,
|
||||||
src_width, src_height, 1);
|
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) {
|
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);
|
Draw(bicubic_program, surface.Handle(), filter_fbo.handle, blit.dst_level, blit.dst_rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitHelper::FilterNearest(Surface& surface, const VideoCore::TextureBlit& blit) {
|
void BlitHelper::FilterNearest(Surface& surface, const VideoCore::TextureBlit& blit) {
|
||||||
state.texture_units[2].texture_2d = surface.Handle(false);
|
state.texture_units[2].texture_2d = surface.Handle(0);
|
||||||
SetParams(nearest_program, surface.width, surface.height, blit.src_rect);
|
SetParams(nearest_program, surface.Extent(), blit.src_rect);
|
||||||
Draw(nearest_program, surface.Handle(), filter_fbo.handle, blit.dst_level, blit.dst_rect);
|
Draw(nearest_program, surface.Handle(), filter_fbo.handle, blit.dst_level, blit.dst_rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitHelper::FilterScaleForce(Surface& surface, const VideoCore::TextureBlit& blit) {
|
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);
|
Draw(scale_force_program, surface.Handle(), filter_fbo.handle, blit.dst_level, blit.dst_rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitHelper::FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit) {
|
void BlitHelper::FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit) {
|
||||||
glProgramUniform1f(xbrz_program.handle, 2, static_cast<GLfloat>(surface.res_scale));
|
glProgramUniform1f(xbrz_program.handle, 2, static_cast<GLfloat>(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);
|
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<u32> src_rect) {
|
Common::Rectangle<u32> src_rect) {
|
||||||
glProgramUniform2f(
|
glProgramUniform2f(
|
||||||
program.handle, 0,
|
program.handle, 0,
|
||||||
static_cast<float>(src_rect.right - src_rect.left) / static_cast<float>(src_width),
|
static_cast<float>(src_rect.right - src_rect.left) / static_cast<float>(src_extent.width),
|
||||||
static_cast<float>(src_rect.top - src_rect.bottom) / static_cast<float>(src_height));
|
static_cast<float>(src_rect.top - src_rect.bottom) / static_cast<float>(src_extent.height));
|
||||||
glProgramUniform2f(program.handle, 1,
|
glProgramUniform2f(program.handle, 1,
|
||||||
static_cast<float>(src_rect.left) / static_cast<float>(src_width),
|
static_cast<float>(src_rect.left) / static_cast<float>(src_extent.width),
|
||||||
static_cast<float>(src_rect.bottom) / static_cast<float>(src_height));
|
static_cast<float>(src_rect.bottom) / static_cast<float>(src_extent.height));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitHelper::Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 dst_level,
|
void BlitHelper::Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 dst_level,
|
||||||
Common::Rectangle<u32> dst_rect) {
|
Common::Rectangle<u32> dst_rect) {
|
||||||
state.draw.draw_framebuffer = dst_fbo;
|
state.draw.draw_framebuffer = dst_fbo;
|
||||||
state.draw.shader_program = program.handle;
|
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.x = dst_rect.left;
|
||||||
state.viewport.y = dst_rect.bottom;
|
state.viewport.y = dst_rect.bottom;
|
||||||
state.viewport.width = dst_rect.GetWidth();
|
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);
|
dst_level);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,8 +9,9 @@
|
|||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
|
||||||
namespace VideoCore {
|
namespace VideoCore {
|
||||||
|
struct Extent;
|
||||||
struct TextureBlit;
|
struct TextureBlit;
|
||||||
}
|
} // namespace VideoCore
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ private:
|
|||||||
|
|
||||||
void FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit);
|
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<u32> src_rect);
|
Common::Rectangle<u32> src_rect);
|
||||||
|
|
||||||
void Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 dst_level,
|
void Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 dst_level,
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "core/telemetry_session.h"
|
#include "core/telemetry_session.h"
|
||||||
|
#include "video_core/custom_textures/custom_format.h"
|
||||||
#include "video_core/renderer_opengl/gl_driver.h"
|
#include "video_core/renderer_opengl/gl_driver.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
@ -103,6 +104,25 @@ bool Driver::HasDebugTool() {
|
|||||||
return false;
|
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() {
|
void Driver::ReportDriverInfo() {
|
||||||
// Report the context version and the vendor string
|
// Report the context version and the vendor string
|
||||||
gl_version = std::string_view{reinterpret_cast<const char*>(glGetString(GL_VERSION))};
|
gl_version = std::string_view{reinterpret_cast<const char*>(glGetString(GL_VERSION))};
|
||||||
@ -145,7 +165,9 @@ void Driver::CheckExtensionSupport() {
|
|||||||
arb_buffer_storage = GLAD_GL_ARB_buffer_storage;
|
arb_buffer_storage = GLAD_GL_ARB_buffer_storage;
|
||||||
arb_clear_texture = GLAD_GL_ARB_clear_texture;
|
arb_clear_texture = GLAD_GL_ARB_clear_texture;
|
||||||
arb_get_texture_sub_image = GLAD_GL_ARB_get_texture_sub_image;
|
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_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;
|
is_suitable = GLAD_GL_VERSION_4_3 || GLAD_GL_ES_VERSION_3_1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,10 @@ namespace Core {
|
|||||||
class TelemetrySession;
|
class TelemetrySession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
enum class CustomPixelFormat : u32;
|
||||||
|
}
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
enum class Vendor {
|
enum class Vendor {
|
||||||
@ -51,6 +55,9 @@ public:
|
|||||||
/// Returns true if any debug tool is attached
|
/// Returns true if any debug tool is attached
|
||||||
bool HasDebugTool();
|
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
|
/// Returns the vendor of the currently selected physical device
|
||||||
Vendor GetVendor() const {
|
Vendor GetVendor() const {
|
||||||
return vendor;
|
return vendor;
|
||||||
@ -114,6 +121,8 @@ private:
|
|||||||
bool arb_clear_texture{};
|
bool arb_clear_texture{};
|
||||||
bool arb_get_texture_sub_image{};
|
bool arb_get_texture_sub_image{};
|
||||||
bool ext_clip_cull_distance{};
|
bool ext_clip_cull_distance{};
|
||||||
|
bool ext_texture_compression_s3tc{};
|
||||||
|
bool arb_texture_compression_bptc{};
|
||||||
|
|
||||||
std::string_view gl_version{};
|
std::string_view gl_version{};
|
||||||
std::string_view gpu_vendor{};
|
std::string_view gpu_vendor{};
|
||||||
|
@ -73,11 +73,13 @@ GLenum MakeAttributeType(Pica::PipelineRegs::VertexAttributeFormat format) {
|
|||||||
|
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
RasterizerOpenGL::RasterizerOpenGL(Memory::MemorySystem& memory, VideoCore::RendererBase& renderer,
|
RasterizerOpenGL::RasterizerOpenGL(Memory::MemorySystem& memory,
|
||||||
Driver& driver_)
|
VideoCore::CustomTexManager& custom_tex_manager,
|
||||||
|
VideoCore::RendererBase& renderer, Driver& driver_)
|
||||||
: VideoCore::RasterizerAccelerated{memory}, driver{driver_}, runtime{driver, renderer},
|
: VideoCore::RasterizerAccelerated{memory}, driver{driver_}, runtime{driver, renderer},
|
||||||
res_cache{memory, runtime, regs, renderer}, texture_buffer_size{TextureBufferSize()},
|
res_cache{memory, custom_tex_manager, runtime, regs, renderer},
|
||||||
vertex_buffer{driver, GL_ARRAY_BUFFER, VERTEX_BUFFER_SIZE},
|
texture_buffer_size{TextureBufferSize()}, vertex_buffer{driver, GL_ARRAY_BUFFER,
|
||||||
|
VERTEX_BUFFER_SIZE},
|
||||||
uniform_buffer{driver, GL_UNIFORM_BUFFER, UNIFORM_BUFFER_SIZE},
|
uniform_buffer{driver, GL_UNIFORM_BUFFER, UNIFORM_BUFFER_SIZE},
|
||||||
index_buffer{driver, GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE},
|
index_buffer{driver, GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE},
|
||||||
texture_buffer{driver, GL_TEXTURE_BUFFER, texture_buffer_size}, texture_lf_buffer{
|
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;
|
RasterizerOpenGL::~RasterizerOpenGL() = default;
|
||||||
|
|
||||||
|
void RasterizerOpenGL::TickFrame() {
|
||||||
|
res_cache.TickFrame();
|
||||||
|
}
|
||||||
|
|
||||||
void RasterizerOpenGL::LoadDiskResources(const std::atomic_bool& stop_loading,
|
void RasterizerOpenGL::LoadDiskResources(const std::atomic_bool& stop_loading,
|
||||||
const VideoCore::DiskResourceLoadCallback& callback) {
|
const VideoCore::DiskResourceLoadCallback& callback) {
|
||||||
shader_program_manager->LoadDiskCache(stop_loading, 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.y = draw_rect.bottom;
|
||||||
state.scissor.width = draw_rect.GetWidth();
|
state.scissor.width = draw_rect.GetWidth();
|
||||||
state.scissor.height = draw_rect.GetHeight();
|
state.scissor.height = draw_rect.GetHeight();
|
||||||
state.Apply();
|
|
||||||
|
|
||||||
const u32 res_scale = framebuffer.ResolutionScale();
|
const u32 res_scale = framebuffer.ResolutionScale();
|
||||||
if (uniform_block_data.data.framebuffer_scale != res_scale) {
|
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
|
// Sync and bind the texture surfaces
|
||||||
SyncTextureUnits(framebuffer);
|
SyncTextureUnits(framebuffer);
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
// Sync and bind the shader
|
// Sync and bind the shader
|
||||||
if (shader_dirty) {
|
if (shader_dirty) {
|
||||||
SetShader();
|
shader_program_manager->UseFragmentShader(regs, use_custom_normal);
|
||||||
shader_dirty = false;
|
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.
|
// on the male character's face, which in the OpenGL default appear black.
|
||||||
state.texture_units[texture_index].texture_2d = default_texture;
|
state.texture_units[texture_index].texture_2d = default_texture;
|
||||||
} else if (!IsFeedbackLoop(texture_index, framebuffer, *surface)) {
|
} else if (!IsFeedbackLoop(texture_index, framebuffer, *surface)) {
|
||||||
|
BindMaterial(texture_index, *surface);
|
||||||
state.texture_units[texture_index].texture_2d = surface->Handle();
|
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;
|
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,
|
bool RasterizerOpenGL::IsFeedbackLoop(u32 texture_index, const Framebuffer& framebuffer,
|
||||||
Surface& surface) {
|
Surface& surface) {
|
||||||
const GLuint color_attachment = framebuffer.Attachment(SurfaceType::Color);
|
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_pz = 0;
|
||||||
state.image_shadow_texture_nz = 0;
|
state.image_shadow_texture_nz = 0;
|
||||||
state.image_shadow_buffer = 0;
|
state.image_shadow_buffer = 0;
|
||||||
state.Apply();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RasterizerOpenGL::NotifyFixedFunctionPicaRegisterChanged(u32 id) {
|
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() {
|
void RasterizerOpenGL::SyncClipEnabled() {
|
||||||
state.clip_distance[1] = Pica::g_state.regs.rasterizer.clip_enable != 0;
|
state.clip_distance[1] = Pica::g_state.regs.rasterizer.clip_enable != 0;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,10 @@ namespace VideoCore {
|
|||||||
class RendererBase;
|
class RendererBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace VideoCore {
|
||||||
|
class CustomTexManager;
|
||||||
|
}
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
class Driver;
|
class Driver;
|
||||||
@ -25,10 +29,12 @@ class ShaderProgramManager;
|
|||||||
|
|
||||||
class RasterizerOpenGL : public VideoCore::RasterizerAccelerated {
|
class RasterizerOpenGL : public VideoCore::RasterizerAccelerated {
|
||||||
public:
|
public:
|
||||||
explicit RasterizerOpenGL(Memory::MemorySystem& memory, VideoCore::RendererBase& renderer,
|
explicit RasterizerOpenGL(Memory::MemorySystem& memory,
|
||||||
Driver& driver);
|
VideoCore::CustomTexManager& custom_tex_manager,
|
||||||
|
VideoCore::RendererBase& renderer, Driver& driver);
|
||||||
~RasterizerOpenGL() override;
|
~RasterizerOpenGL() override;
|
||||||
|
|
||||||
|
void TickFrame();
|
||||||
void LoadDiskResources(const std::atomic_bool& stop_loading,
|
void LoadDiskResources(const std::atomic_bool& stop_loading,
|
||||||
const VideoCore::DiskResourceLoadCallback& callback) override;
|
const VideoCore::DiskResourceLoadCallback& callback) override;
|
||||||
|
|
||||||
@ -74,9 +80,6 @@ private:
|
|||||||
/// Syncs the clip enabled status to match the PICA register
|
/// Syncs the clip enabled status to match the PICA register
|
||||||
void SyncClipEnabled();
|
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
|
/// Syncs the cull mode to match the PICA register
|
||||||
void SyncCullMode();
|
void SyncCullMode();
|
||||||
|
|
||||||
@ -126,6 +129,9 @@ private:
|
|||||||
/// Unbinds all special texture unit 0 texture configurations
|
/// Unbinds all special texture unit 0 texture configurations
|
||||||
void UnbindSpecial();
|
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
|
/// Upload the uniform blocks to the uniform buffer object
|
||||||
void UploadUniforms(bool accelerate_draw);
|
void UploadUniforms(bool accelerate_draw);
|
||||||
|
|
||||||
@ -174,6 +180,7 @@ private:
|
|||||||
OGLTexture texture_buffer_lut_lf;
|
OGLTexture texture_buffer_lut_lf;
|
||||||
OGLTexture texture_buffer_lut_rg;
|
OGLTexture texture_buffer_lut_rg;
|
||||||
OGLTexture texture_buffer_lut_rgba;
|
OGLTexture texture_buffer_lut_rgba;
|
||||||
|
bool use_custom_normal{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
@ -59,7 +59,7 @@ out gl_PerVertex {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
PicaFSConfig PicaFSConfig::BuildFromRegs(const Pica::Regs& regs) {
|
PicaFSConfig PicaFSConfig::BuildFromRegs(const Pica::Regs& regs, bool use_normal) {
|
||||||
PicaFSConfig res{};
|
PicaFSConfig res{};
|
||||||
|
|
||||||
auto& state = res.state;
|
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.shadow_texture_orthographic = regs.texturing.shadow.orthographic != 0;
|
||||||
|
|
||||||
|
state.use_custom_normal_map = use_normal;
|
||||||
|
|
||||||
return res;
|
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");
|
LOG_DEBUG(Render_OpenGL, "Using Texture3 without enabling it");
|
||||||
return "vec4(0.0)";
|
return "vec4(0.0)";
|
||||||
}
|
}
|
||||||
|
case 4:
|
||||||
|
return "texture(tex_normal, texcoord0)";
|
||||||
default:
|
default:
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
return "";
|
return "";
|
||||||
@ -642,7 +646,12 @@ static void WriteLighting(std::string& out, const PicaFSConfig& config) {
|
|||||||
const auto Perturbation = [&] {
|
const auto Perturbation = [&] {
|
||||||
return fmt::format("2.0 * ({}).rgb - 1.0", SampleTexture(config, lighting.bump_selector));
|
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
|
// Bump mapping is enabled using a normal map
|
||||||
out += fmt::format("vec3 surface_normal = {};\n", Perturbation());
|
out += fmt::format("vec3 surface_normal = {};\n", Perturbation());
|
||||||
|
|
||||||
@ -1207,6 +1216,7 @@ out vec4 color;
|
|||||||
uniform sampler2D tex0;
|
uniform sampler2D tex0;
|
||||||
uniform sampler2D tex1;
|
uniform sampler2D tex1;
|
||||||
uniform sampler2D tex2;
|
uniform sampler2D tex2;
|
||||||
|
uniform sampler2D tex_normal; //< Used for custom normal maps
|
||||||
uniform samplerCube tex_cube;
|
uniform samplerCube tex_cube;
|
||||||
uniform samplerBuffer texture_buffer_lut_lf;
|
uniform samplerBuffer texture_buffer_lut_lf;
|
||||||
uniform samplerBuffer texture_buffer_lut_rg;
|
uniform samplerBuffer texture_buffer_lut_rg;
|
||||||
|
@ -117,6 +117,7 @@ struct PicaFSConfigState {
|
|||||||
|
|
||||||
bool shadow_rendering;
|
bool shadow_rendering;
|
||||||
bool shadow_texture_orthographic;
|
bool shadow_texture_orthographic;
|
||||||
|
bool use_custom_normal_map;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -130,7 +131,7 @@ struct PicaFSConfigState {
|
|||||||
struct PicaFSConfig : Common::HashableStruct<PicaFSConfigState> {
|
struct PicaFSConfig : Common::HashableStruct<PicaFSConfigState> {
|
||||||
|
|
||||||
/// Construct a PicaFSConfig with the given Pica register configuration.
|
/// 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 {
|
bool TevStageUpdatesCombinerBufferColor(unsigned stage_index) const {
|
||||||
return (stage_index < 4) && (state.combiner_buffer_input & (1 << stage_index));
|
return (stage_index < 4) && (state.combiner_buffer_input & (1 << stage_index));
|
||||||
|
@ -133,6 +133,7 @@ static void SetShaderSamplerBindings(GLuint shader) {
|
|||||||
SetShaderSamplerBinding(shader, "tex1", TextureUnits::PicaTexture(1));
|
SetShaderSamplerBinding(shader, "tex1", TextureUnits::PicaTexture(1));
|
||||||
SetShaderSamplerBinding(shader, "tex2", TextureUnits::PicaTexture(2));
|
SetShaderSamplerBinding(shader, "tex2", TextureUnits::PicaTexture(2));
|
||||||
SetShaderSamplerBinding(shader, "tex_cube", TextureUnits::TextureCube);
|
SetShaderSamplerBinding(shader, "tex_cube", TextureUnits::TextureCube);
|
||||||
|
SetShaderSamplerBinding(shader, "tex_normal", TextureUnits::TextureNormalMap);
|
||||||
|
|
||||||
// Set the texture samplers to correspond to different lookup table texture units
|
// Set the texture samplers to correspond to different lookup table texture units
|
||||||
SetShaderSamplerBinding(shader, "texture_buffer_lut_lf", TextureUnits::TextureBufferLUT_LF);
|
SetShaderSamplerBinding(shader, "texture_buffer_lut_lf", TextureUnits::TextureBufferLUT_LF);
|
||||||
@ -415,8 +416,8 @@ void ShaderProgramManager::UseTrivialGeometryShader() {
|
|||||||
impl->current.gs_hash = 0;
|
impl->current.gs_hash = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShaderProgramManager::UseFragmentShader(const Pica::Regs& regs) {
|
void ShaderProgramManager::UseFragmentShader(const Pica::Regs& regs, bool use_normal) {
|
||||||
PicaFSConfig config = PicaFSConfig::BuildFromRegs(regs);
|
PicaFSConfig config = PicaFSConfig::BuildFromRegs(regs, use_normal);
|
||||||
auto [handle, result] = impl->fragment_shaders.Get(config);
|
auto [handle, result] = impl->fragment_shaders.Get(config);
|
||||||
impl->current.fs = handle;
|
impl->current.fs = handle;
|
||||||
impl->current.fs_hash = config.Hash();
|
impl->current.fs_hash = config.Hash();
|
||||||
|
@ -41,7 +41,7 @@ public:
|
|||||||
|
|
||||||
void UseTrivialGeometryShader();
|
void UseTrivialGeometryShader();
|
||||||
|
|
||||||
void UseFragmentShader(const Pica::Regs& config);
|
void UseFragmentShader(const Pica::Regs& config, bool use_normal);
|
||||||
|
|
||||||
void ApplyTo(OpenGLState& state);
|
void ApplyTo(OpenGLState& state);
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ constexpr TextureUnit TextureCube{6};
|
|||||||
constexpr TextureUnit TextureBufferLUT_LF{3};
|
constexpr TextureUnit TextureBufferLUT_LF{3};
|
||||||
constexpr TextureUnit TextureBufferLUT_RG{4};
|
constexpr TextureUnit TextureBufferLUT_RG{4};
|
||||||
constexpr TextureUnit TextureBufferLUT_RGBA{5};
|
constexpr TextureUnit TextureBufferLUT_RGBA{5};
|
||||||
|
constexpr TextureUnit TextureNormalMap{7};
|
||||||
|
|
||||||
} // namespace TextureUnits
|
} // namespace TextureUnits
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "common/scope_exit.h"
|
#include "common/scope_exit.h"
|
||||||
#include "common/settings.h"
|
#include "video_core/custom_textures/material.h"
|
||||||
#include "video_core/regs.h"
|
#include "video_core/regs.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/renderer_opengl/gl_driver.h"
|
#include "video_core/renderer_opengl/gl_driver.h"
|
||||||
@ -14,8 +14,10 @@ namespace OpenGL {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
using VideoCore::MapType;
|
||||||
using VideoCore::PixelFormat;
|
using VideoCore::PixelFormat;
|
||||||
using VideoCore::SurfaceType;
|
using VideoCore::SurfaceType;
|
||||||
|
using VideoCore::TextureType;
|
||||||
|
|
||||||
constexpr FormatTuple DEFAULT_TUPLE = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE};
|
constexpr FormatTuple DEFAULT_TUPLE = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE};
|
||||||
|
|
||||||
@ -42,6 +44,17 @@ static constexpr std::array<FormatTuple, 5> COLOR_TUPLES_OES = {{
|
|||||||
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4}, // RGBA4
|
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4}, // RGBA4
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
static constexpr std::array<FormatTuple, 8> 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 {
|
struct FramebufferInfo {
|
||||||
GLuint color;
|
GLuint color;
|
||||||
GLuint depth;
|
GLuint depth;
|
||||||
@ -109,17 +122,23 @@ TextureRuntime::TextureRuntime(const Driver& driver_, VideoCore::RendererBase& r
|
|||||||
read_fbos[i].Create();
|
read_fbos[i].Create();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Register = [this](PixelFormat dest, std::unique_ptr<FormatReinterpreterBase>&& obj) {
|
auto add_reinterpreter = [this](PixelFormat dest,
|
||||||
|
std::unique_ptr<FormatReinterpreterBase>&& obj) {
|
||||||
const u32 dst_index = static_cast<u32>(dest);
|
const u32 dst_index = static_cast<u32>(dest);
|
||||||
return reinterpreters[dst_index].push_back(std::move(obj));
|
return reinterpreters[dst_index].push_back(std::move(obj));
|
||||||
};
|
};
|
||||||
|
|
||||||
Register(PixelFormat::RGBA8, std::make_unique<ShaderD24S8toRGBA8>());
|
add_reinterpreter(PixelFormat::RGBA8, std::make_unique<ShaderD24S8toRGBA8>());
|
||||||
Register(PixelFormat::RGB5A1, std::make_unique<RGBA4toRGB5A1>());
|
add_reinterpreter(PixelFormat::RGB5A1, std::make_unique<RGBA4toRGB5A1>());
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureRuntime::~TextureRuntime() = default;
|
TextureRuntime::~TextureRuntime() = default;
|
||||||
|
|
||||||
|
void TextureRuntime::Reset() {
|
||||||
|
alloc_cache.clear();
|
||||||
|
framebuffer_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool TextureRuntime::NeedsConversion(VideoCore::PixelFormat pixel_format) const {
|
bool TextureRuntime::NeedsConversion(VideoCore::PixelFormat pixel_format) const {
|
||||||
const bool should_convert = pixel_format == PixelFormat::RGBA8 || // Needs byteswap
|
const bool should_convert = pixel_format == PixelFormat::RGBA8 || // Needs byteswap
|
||||||
pixel_format == PixelFormat::RGB8; // Is converted to RGBA8
|
pixel_format == PixelFormat::RGB8; // Is converted to RGBA8
|
||||||
@ -153,51 +172,64 @@ const FormatTuple& TextureRuntime::GetFormatTuple(PixelFormat pixel_format) cons
|
|||||||
return DEFAULT_TUPLE;
|
return DEFAULT_TUPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureRuntime::Recycle(const HostTextureTag tag, Allocation&& alloc) {
|
const FormatTuple& TextureRuntime::GetFormatTuple(VideoCore::CustomPixelFormat pixel_format) {
|
||||||
recycler.emplace(tag, std::move(alloc));
|
const std::size_t format_index = static_cast<std::size_t>(pixel_format);
|
||||||
|
return CUSTOM_TUPLES[format_index];
|
||||||
}
|
}
|
||||||
|
|
||||||
Allocation TextureRuntime::Allocate(const VideoCore::SurfaceParams& params) {
|
void TextureRuntime::Recycle(const HostTextureTag tag, Allocation&& alloc) {
|
||||||
const u32 width = params.width;
|
alloc_cache.emplace(tag, std::move(alloc));
|
||||||
const u32 height = params.height;
|
}
|
||||||
const u32 levels = params.levels;
|
|
||||||
|
Allocation TextureRuntime::Allocate(const VideoCore::SurfaceParams& params,
|
||||||
|
const VideoCore::Material* material) {
|
||||||
const GLenum target = params.texture_type == VideoCore::TextureType::CubeMap
|
const GLenum target = params.texture_type == VideoCore::TextureType::CubeMap
|
||||||
? GL_TEXTURE_CUBE_MAP
|
? GL_TEXTURE_CUBE_MAP
|
||||||
: GL_TEXTURE_2D;
|
: 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 = {
|
const HostTextureTag key = {
|
||||||
|
.width = params.width,
|
||||||
|
.height = params.height,
|
||||||
|
.levels = params.levels,
|
||||||
|
.res_scale = params.res_scale,
|
||||||
.tuple = tuple,
|
.tuple = tuple,
|
||||||
.type = params.texture_type,
|
.type = params.texture_type,
|
||||||
.width = width,
|
.is_custom = is_custom,
|
||||||
.height = height,
|
.has_normal = has_normal,
|
||||||
.levels = levels,
|
|
||||||
.res_scale = params.res_scale,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (auto it = recycler.find(key); it != recycler.end()) {
|
if (auto it = alloc_cache.find(key); it != alloc_cache.end()) {
|
||||||
Allocation alloc = std::move(it->second);
|
auto alloc{std::move(it->second)};
|
||||||
ASSERT(alloc.res_scale == params.res_scale);
|
alloc_cache.erase(it);
|
||||||
recycler.erase(it);
|
|
||||||
return alloc;
|
return alloc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLuint old_tex = OpenGLState::GetCurState().texture_units[0].texture_2d;
|
const GLuint old_tex = OpenGLState::GetCurState().texture_units[0].texture_2d;
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
std::array<OGLTexture, 2> textures{};
|
std::array<OGLTexture, 3> textures{};
|
||||||
std::array<GLuint, 2> handles{};
|
std::array<GLuint, 3> 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);
|
handles.fill(textures[0].handle);
|
||||||
|
|
||||||
if (params.res_scale != 1) {
|
if (params.res_scale != 1) {
|
||||||
const u32 scaled_width = params.GetScaledWidth();
|
const u32 scaled_width = is_custom ? params.width : params.GetScaledWidth();
|
||||||
const u32 scaled_height = params.GetScaledHeight();
|
const u32 scaled_height = is_custom ? params.height : params.GetScaledHeight();
|
||||||
textures[1] =
|
const auto& scaled_tuple = is_custom ? GetFormatTuple(PixelFormat::RGBA8) : tuple;
|
||||||
MakeHandle(target, scaled_width, scaled_height, levels, tuple, params.DebugName(true));
|
textures[1] = MakeHandle(target, scaled_width, scaled_height, params.levels, scaled_tuple,
|
||||||
|
params.DebugName(true, is_custom));
|
||||||
handles[1] = textures[1].handle;
|
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);
|
glBindTexture(GL_TEXTURE_2D, old_tex);
|
||||||
|
|
||||||
@ -311,15 +343,20 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextureRuntime::GenerateMipmaps(Surface& surface, u32 max_level) {
|
void TextureRuntime::GenerateMipmaps(Surface& surface) {
|
||||||
OpenGLState state = OpenGLState::GetCurState();
|
OpenGLState state = OpenGLState::GetCurState();
|
||||||
state.texture_units[0].texture_2d = surface.Handle();
|
|
||||||
state.Apply();
|
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
const auto generate = [&](u32 index) {
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, max_level);
|
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(
|
const ReinterpreterList& TextureRuntime::GetPossibleReinterpretations(
|
||||||
@ -340,21 +377,11 @@ Surface::~Surface() {
|
|||||||
if (pixel_format == PixelFormat::Invalid || !alloc) {
|
if (pixel_format == PixelFormat::Invalid || !alloc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
runtime->Recycle(MakeTag(), std::move(alloc));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Surface::Upload(const VideoCore::BufferTextureCopy& upload,
|
void Surface::Upload(const VideoCore::BufferTextureCopy& upload,
|
||||||
const VideoCore::StagingData& staging) {
|
const VideoCore::StagingData& staging) {
|
||||||
// Ensure no bad interactions with GL_UNPACK_ALIGNMENT
|
|
||||||
ASSERT(stride * GetFormatBytesPerPixel(pixel_format) % 4 == 0);
|
ASSERT(stride * GetFormatBytesPerPixel(pixel_format) % 4 == 0);
|
||||||
|
|
||||||
const GLuint old_tex = OpenGLState::GetCurState().texture_units[0].texture_2d;
|
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();
|
const u32 unscaled_height = upload.texture_rect.GetHeight();
|
||||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, unscaled_width);
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, unscaled_width);
|
||||||
|
|
||||||
// Bind the unscaled texture
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
glBindTexture(GL_TEXTURE_2D, Handle(false));
|
glBindTexture(GL_TEXTURE_2D, Handle(0));
|
||||||
|
|
||||||
// Upload the requested rectangle of pixels
|
const auto& tuple = alloc.tuple;
|
||||||
const auto& tuple = runtime->GetFormatTuple(pixel_format);
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, upload.texture_level, upload.texture_rect.left,
|
glTexSubImage2D(GL_TEXTURE_2D, upload.texture_level, upload.texture_rect.left,
|
||||||
upload.texture_rect.bottom, unscaled_width, unscaled_height, tuple.format,
|
upload.texture_rect.bottom, unscaled_width, unscaled_height, tuple.format,
|
||||||
tuple.type, staging.mapped.data());
|
tuple.type, staging.mapped.data());
|
||||||
@ -381,17 +406,61 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload,
|
|||||||
.src_rect = upload.texture_rect,
|
.src_rect = upload.texture_rect,
|
||||||
.dst_rect = upload.texture_rect * res_scale,
|
.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)) {
|
if (res_scale != 1 && !runtime->blit_helper.Filter(*this, blit)) {
|
||||||
BlitScale(blit, true);
|
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<GLsizei>(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<GLsizei>(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,
|
void Surface::Download(const VideoCore::BufferTextureCopy& download,
|
||||||
const VideoCore::StagingData& staging) {
|
const VideoCore::StagingData& staging) {
|
||||||
// Ensure no bad interactions with GL_PACK_ALIGNMENT
|
|
||||||
ASSERT(stride * GetFormatBytesPerPixel(pixel_format) % 4 == 0);
|
ASSERT(stride * GetFormatBytesPerPixel(pixel_format) % 4 == 0);
|
||||||
|
|
||||||
const u32 unscaled_width = download.texture_rect.GetWidth();
|
const u32 unscaled_width = download.texture_rect.GetWidth();
|
||||||
@ -414,11 +483,9 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const u32 fbo_index = FboIndex(type);
|
|
||||||
|
|
||||||
OpenGLState state = OpenGLState::GetCurState();
|
OpenGLState state = OpenGLState::GetCurState();
|
||||||
state.scissor.enabled = false;
|
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();
|
state.Apply();
|
||||||
|
|
||||||
Attach(GL_READ_FRAMEBUFFER, download.texture_level, 0, false);
|
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,
|
glReadPixels(download.texture_rect.left, download.texture_rect.bottom, unscaled_width,
|
||||||
unscaled_height, tuple.format, tuple.type, staging.mapped.data());
|
unscaled_height, tuple.format, tuple.type, staging.mapped.data());
|
||||||
|
|
||||||
// Restore previous state
|
|
||||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Surface::DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download,
|
bool Surface::DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download,
|
||||||
const VideoCore::StagingData& staging) {
|
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 is_full_download = download.texture_rect == GetRect();
|
||||||
const bool has_sub_image = driver->HasArbGetTextureSubImage();
|
const bool has_sub_image = driver->HasArbGetTextureSubImage();
|
||||||
if (driver->IsOpenGLES() || (!is_full_download && !has_sub_image)) {
|
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
|
// Prefer glGetTextureSubImage in most cases since it's the fastest and most convenient option
|
||||||
if (has_sub_image) {
|
if (has_sub_image) {
|
||||||
const GLsizei buf_size = static_cast<GLsizei>(staging.mapped.size());
|
const GLsizei buf_size = static_cast<GLsizei>(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.bottom, 0, download.texture_rect.GetWidth(),
|
||||||
download.texture_rect.GetHeight(), 1, tuple.format, tuple.type,
|
download.texture_rect.GetHeight(), 1, tuple.format, tuple.type,
|
||||||
buf_size, staging.mapped.data());
|
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
|
// This should only trigger for full texture downloads in oldish intel drivers
|
||||||
// that only support up to 4.3
|
// 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,
|
glGetTexImage(GL_TEXTURE_2D, download.texture_level, tuple.format, tuple.type,
|
||||||
staging.mapped.data());
|
staging.mapped.data());
|
||||||
glBindTexture(GL_TEXTURE_2D, old_tex);
|
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) {
|
void Surface::Attach(GLenum target, u32 level, u32 layer, bool scaled) {
|
||||||
const GLuint handle = Handle(scaled);
|
const GLuint handle = Handle(static_cast<u32>(scaled));
|
||||||
const GLenum textarget = texture_type == VideoCore::TextureType::CubeMap
|
const GLenum textarget = texture_type == TextureType::CubeMap
|
||||||
? GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer
|
? GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer
|
||||||
: GL_TEXTURE_2D;
|
: GL_TEXTURE_2D;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case VideoCore::SurfaceType::Color:
|
case SurfaceType::Color:
|
||||||
case VideoCore::SurfaceType::Texture:
|
case SurfaceType::Texture:
|
||||||
glFramebufferTexture2D(target, GL_COLOR_ATTACHMENT0, textarget, handle, level);
|
glFramebufferTexture2D(target, GL_COLOR_ATTACHMENT0, textarget, handle, level);
|
||||||
break;
|
break;
|
||||||
case VideoCore::SurfaceType::Depth:
|
case SurfaceType::Depth:
|
||||||
glFramebufferTexture2D(target, GL_DEPTH_ATTACHMENT, textarget, handle, level);
|
glFramebufferTexture2D(target, GL_DEPTH_ATTACHMENT, textarget, handle, level);
|
||||||
break;
|
break;
|
||||||
case VideoCore::SurfaceType::DepthStencil:
|
case SurfaceType::DepthStencil:
|
||||||
glFramebufferTexture2D(target, GL_DEPTH_STENCIL_ATTACHMENT, textarget, handle, level);
|
glFramebufferTexture2D(target, GL_DEPTH_STENCIL_ATTACHMENT, textarget, handle, level);
|
||||||
break;
|
break;
|
||||||
default:
|
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 {
|
u32 Surface::GetInternalBytesPerPixel() const {
|
||||||
// RGB8 is converted to RGBA8 on OpenGL ES since it doesn't support BGR8
|
// RGB8 is converted to RGBA8 on OpenGL ES since it doesn't support BGR8
|
||||||
if (driver->IsOpenGLES() && pixel_format == VideoCore::PixelFormat::RGB8) {
|
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);
|
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,
|
Framebuffer::Framebuffer(TextureRuntime& runtime, Surface* const color, u32 color_level,
|
||||||
Surface* const depth_stencil, u32 depth_level, const Pica::Regs& regs,
|
Surface* const depth_stencil, u32 depth_level, const Pica::Regs& regs,
|
||||||
Common::Rectangle<u32> surfaces_rect)
|
Common::Rectangle<u32> surfaces_rect)
|
||||||
@ -527,7 +628,7 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, Surface* const color, u32 colo
|
|||||||
const bool shadow_rendering = regs.framebuffer.IsShadowRendering();
|
const bool shadow_rendering = regs.framebuffer.IsShadowRendering();
|
||||||
const bool has_stencil = regs.framebuffer.HasStencil();
|
const bool has_stencil = regs.framebuffer.HasStencil();
|
||||||
if (shadow_rendering && !color) {
|
if (shadow_rendering && !color) {
|
||||||
return; // Framebuffer won't get used
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
@ -574,19 +675,15 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, Surface* const color, u32 colo
|
|||||||
color ? color->Handle() : 0, color_level);
|
color ? color->Handle() : 0, color_level);
|
||||||
if (depth_stencil) {
|
if (depth_stencil) {
|
||||||
if (has_stencil) {
|
if (has_stencil) {
|
||||||
// Attach both depth and stencil
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
|
||||||
GL_TEXTURE_2D, depth_stencil->Handle(), depth_level);
|
GL_TEXTURE_2D, depth_stencil->Handle(), depth_level);
|
||||||
} else {
|
} else {
|
||||||
// Attach depth
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
depth_stencil->Handle(), depth_level);
|
depth_stencil->Handle(), depth_level);
|
||||||
// Clear stencil attachment
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clear both depth and stencil attachment
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
0, 0);
|
0, 0);
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,9 @@
|
|||||||
#include "video_core/renderer_opengl/gl_format_reinterpreter.h"
|
#include "video_core/renderer_opengl/gl_format_reinterpreter.h"
|
||||||
|
|
||||||
namespace VideoCore {
|
namespace VideoCore {
|
||||||
|
struct Material;
|
||||||
class RendererBase;
|
class RendererBase;
|
||||||
}
|
} // namespace VideoCore
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
@ -27,17 +28,19 @@ struct FormatTuple {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct HostTextureTag {
|
struct HostTextureTag {
|
||||||
FormatTuple tuple{};
|
u32 width;
|
||||||
VideoCore::TextureType type{};
|
u32 height;
|
||||||
u32 width = 0;
|
u32 levels;
|
||||||
u32 height = 0;
|
u32 res_scale;
|
||||||
u32 levels = 1;
|
FormatTuple tuple;
|
||||||
u32 res_scale = 1;
|
VideoCore::TextureType type;
|
||||||
|
bool is_custom;
|
||||||
|
bool has_normal;
|
||||||
|
|
||||||
bool operator==(const HostTextureTag& other) const noexcept {
|
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,
|
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 {
|
struct Hash {
|
||||||
@ -46,10 +49,12 @@ struct HostTextureTag {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
static_assert(std::has_unique_object_representations_v<HostTextureTag>,
|
||||||
|
"HostTextureTag is not suitable for hashing!");
|
||||||
|
|
||||||
struct Allocation {
|
struct Allocation {
|
||||||
std::array<OGLTexture, 2> textures;
|
std::array<OGLTexture, 3> textures;
|
||||||
std::array<GLuint, 2> handles;
|
std::array<GLuint, 3> handles;
|
||||||
FormatTuple tuple;
|
FormatTuple tuple;
|
||||||
u32 width;
|
u32 width;
|
||||||
u32 height;
|
u32 height;
|
||||||
@ -59,15 +64,10 @@ struct Allocation {
|
|||||||
operator bool() const noexcept {
|
operator bool() const noexcept {
|
||||||
return textures[0].handle;
|
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 Surface;
|
||||||
class Driver;
|
class Driver;
|
||||||
struct CachedTextureCube;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides texture manipulation functions to the rasterizer cache
|
* Provides texture manipulation functions to the rasterizer cache
|
||||||
@ -82,6 +82,9 @@ public:
|
|||||||
explicit TextureRuntime(const Driver& driver, VideoCore::RendererBase& renderer);
|
explicit TextureRuntime(const Driver& driver, VideoCore::RendererBase& renderer);
|
||||||
~TextureRuntime();
|
~TextureRuntime();
|
||||||
|
|
||||||
|
/// Clears all cached runtime resources
|
||||||
|
void Reset();
|
||||||
|
|
||||||
/// Returns true if the provided pixel format cannot be used natively by the runtime.
|
/// Returns true if the provided pixel format cannot be used natively by the runtime.
|
||||||
bool NeedsConversion(VideoCore::PixelFormat pixel_format) const;
|
bool NeedsConversion(VideoCore::PixelFormat pixel_format) const;
|
||||||
|
|
||||||
@ -90,12 +93,14 @@ public:
|
|||||||
|
|
||||||
/// Returns the OpenGL format tuple associated with the provided pixel format
|
/// Returns the OpenGL format tuple associated with the provided pixel format
|
||||||
const FormatTuple& GetFormatTuple(VideoCore::PixelFormat pixel_format) const;
|
const FormatTuple& GetFormatTuple(VideoCore::PixelFormat pixel_format) const;
|
||||||
|
const FormatTuple& GetFormatTuple(VideoCore::CustomPixelFormat pixel_format);
|
||||||
|
|
||||||
/// Takes back ownership of the allocation for recycling
|
/// Takes back ownership of the allocation for recycling
|
||||||
void Recycle(const HostTextureTag tag, Allocation&& alloc);
|
void Recycle(const HostTextureTag tag, Allocation&& alloc);
|
||||||
|
|
||||||
/// Allocates an OpenGL texture with the specified dimentions and format
|
/// Allocates a texture with the specified dimentions and format
|
||||||
Allocation Allocate(const VideoCore::SurfaceParams& params);
|
Allocation Allocate(const VideoCore::SurfaceParams& params,
|
||||||
|
const VideoCore::Material* material = nullptr);
|
||||||
|
|
||||||
/// Fills the rectangle of the texture with the clear value provided
|
/// Fills the rectangle of the texture with the clear value provided
|
||||||
bool ClearTexture(Surface& surface, const VideoCore::TextureClear& clear);
|
bool ClearTexture(Surface& surface, const VideoCore::TextureClear& clear);
|
||||||
@ -107,7 +112,7 @@ public:
|
|||||||
bool BlitTextures(Surface& source, Surface& dest, const VideoCore::TextureBlit& blit);
|
bool BlitTextures(Surface& source, Surface& dest, const VideoCore::TextureBlit& blit);
|
||||||
|
|
||||||
/// Generates mipmaps for all the available levels of the texture
|
/// 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
|
/// Returns all source formats that support reinterpretation to the dest format
|
||||||
const ReinterpreterList& GetPossibleReinterpretations(VideoCore::PixelFormat dest_format) const;
|
const ReinterpreterList& GetPossibleReinterpretations(VideoCore::PixelFormat dest_format) const;
|
||||||
@ -123,7 +128,7 @@ private:
|
|||||||
BlitHelper blit_helper;
|
BlitHelper blit_helper;
|
||||||
std::vector<u8> staging_buffer;
|
std::vector<u8> staging_buffer;
|
||||||
std::array<ReinterpreterList, VideoCore::PIXEL_FORMAT_COUNT> reinterpreters;
|
std::array<ReinterpreterList, VideoCore::PIXEL_FORMAT_COUNT> reinterpreters;
|
||||||
std::unordered_multimap<HostTextureTag, Allocation, HostTextureTag::Hash> recycler;
|
std::unordered_multimap<HostTextureTag, Allocation, HostTextureTag::Hash> alloc_cache;
|
||||||
std::unordered_map<u64, OGLFramebuffer, Common::IdentityHash<u64>> framebuffer_cache;
|
std::unordered_map<u64, OGLFramebuffer, Common::IdentityHash<u64>> framebuffer_cache;
|
||||||
std::array<OGLFramebuffer, 3> draw_fbos;
|
std::array<OGLFramebuffer, 3> draw_fbos;
|
||||||
std::array<OGLFramebuffer, 3> read_fbos;
|
std::array<OGLFramebuffer, 3> read_fbos;
|
||||||
@ -140,9 +145,9 @@ public:
|
|||||||
Surface(Surface&& o) noexcept = default;
|
Surface(Surface&& o) noexcept = default;
|
||||||
Surface& operator=(Surface&& o) noexcept = default;
|
Surface& operator=(Surface&& o) noexcept = default;
|
||||||
|
|
||||||
/// Returns the surface image handle
|
/// Returns the surface image handle at the provided index.
|
||||||
GLuint Handle(bool scaled = true) const noexcept {
|
GLuint Handle(u32 index = 1) const noexcept {
|
||||||
return alloc.handles[static_cast<u32>(scaled)];
|
return alloc.handles[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the tuple of the surface allocation.
|
/// Returns the tuple of the surface allocation.
|
||||||
@ -150,9 +155,20 @@ public:
|
|||||||
return alloc.tuple;
|
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
|
/// Uploads pixel data in staging to a rectangle region of the surface texture
|
||||||
void Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging);
|
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
|
/// Downloads pixel data to staging from a rectangle region of the surface texture
|
||||||
void Download(const VideoCore::BufferTextureCopy& download,
|
void Download(const VideoCore::BufferTextureCopy& download,
|
||||||
const VideoCore::StagingData& staging);
|
const VideoCore::StagingData& staging);
|
||||||
@ -160,6 +176,9 @@ public:
|
|||||||
/// Attaches a handle of surface to the specified framebuffer target
|
/// Attaches a handle of surface to the specified framebuffer target
|
||||||
void Attach(GLenum target, u32 level, u32 layer, bool scaled = true);
|
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
|
/// Returns the bpp of the internal surface format
|
||||||
u32 GetInternalBytesPerPixel() const;
|
u32 GetInternalBytesPerPixel() const;
|
||||||
|
|
||||||
@ -171,6 +190,9 @@ private:
|
|||||||
bool DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download,
|
bool DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download,
|
||||||
const VideoCore::StagingData& staging);
|
const VideoCore::StagingData& staging);
|
||||||
|
|
||||||
|
/// Returns the texture tag of the current allocation
|
||||||
|
HostTextureTag MakeTag() const noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const Driver* driver;
|
const Driver* driver;
|
||||||
TextureRuntime* runtime;
|
TextureRuntime* runtime;
|
||||||
|
@ -313,7 +313,8 @@ RendererOpenGL::RendererOpenGL(Core::System& system, Frontend::EmuWindow& window
|
|||||||
}
|
}
|
||||||
frame_dumper.mailbox = std::make_unique<OGLVideoDumpingMailbox>();
|
frame_dumper.mailbox = std::make_unique<OGLVideoDumpingMailbox>();
|
||||||
InitOpenGLObjects();
|
InitOpenGLObjects();
|
||||||
rasterizer = std::make_unique<RasterizerOpenGL>(system.Memory(), *this, driver);
|
rasterizer = std::make_unique<RasterizerOpenGL>(system.Memory(), system.CustomTexManager(),
|
||||||
|
*this, driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
RendererOpenGL::~RendererOpenGL() = default;
|
RendererOpenGL::~RendererOpenGL() = default;
|
||||||
@ -347,6 +348,8 @@ void RendererOpenGL::SwapBuffers() {
|
|||||||
|
|
||||||
EndFrame();
|
EndFrame();
|
||||||
prev_state.Apply();
|
prev_state.Apply();
|
||||||
|
|
||||||
|
rasterizer->TickFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RendererOpenGL::RenderScreenshot() {
|
void RendererOpenGL::RenderScreenshot() {
|
||||||
@ -1169,9 +1172,6 @@ void RendererOpenGL::TryPresent(int timeout_ms, bool is_secondary) {
|
|||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the framerate
|
|
||||||
void RendererOpenGL::UpdateFramerate() {}
|
|
||||||
|
|
||||||
void RendererOpenGL::PrepareVideoDumping() {
|
void RendererOpenGL::PrepareVideoDumping() {
|
||||||
auto* mailbox = static_cast<OGLVideoDumpingMailbox*>(frame_dumper.mailbox.get());
|
auto* mailbox = static_cast<OGLVideoDumpingMailbox*>(frame_dumper.mailbox.get());
|
||||||
{
|
{
|
||||||
|
@ -94,7 +94,6 @@ private:
|
|||||||
float h);
|
float h);
|
||||||
void DrawSingleScreenStereo(const ScreenInfo& screen_info_l, const ScreenInfo& screen_info_r,
|
void DrawSingleScreenStereo(const ScreenInfo& screen_info_l, const ScreenInfo& screen_info_r,
|
||||||
float x, float y, float w, float h);
|
float x, float y, float w, float h);
|
||||||
void UpdateFramerate();
|
|
||||||
|
|
||||||
// Loads framebuffer from emulated memory into the display information structure
|
// Loads framebuffer from emulated memory into the display information structure
|
||||||
void LoadFBToScreenInfo(const GPU::Regs::FramebufferConfig& framebuffer,
|
void LoadFBToScreenInfo(const GPU::Regs::FramebufferConfig& framebuffer,
|
||||||
|
Loading…
Reference in New Issue
Block a user