diff --git a/.gitmodules b/.gitmodules index b43a910ab..0dc4538c1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,3 +43,6 @@ [submodule "teakra"] path = externals/teakra url = https://github.com/wwylele/teakra.git +[submodule "lodepng"] + path = externals/lodepng/lodepng + url = https://github.com/lvandeve/lodepng.git diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index ae92ce861..2ae4c33f9 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -100,3 +100,6 @@ if (ENABLE_WEB_SERVICE) add_library(cpp-jwt INTERFACE) target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include) endif() + +# lodepng +add_subdirectory(lodepng) \ No newline at end of file diff --git a/externals/lodepng/CMakeLists.txt b/externals/lodepng/CMakeLists.txt new file mode 100644 index 000000000..df9a81850 --- /dev/null +++ b/externals/lodepng/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library(lodepng + lodepng/lodepng.cpp + lodepng/lodepng.h +) + +create_target_directory_groups(lodepng) +target_include_directories(lodepng INTERFACE lodepng) diff --git a/externals/lodepng/lodepng b/externals/lodepng/lodepng new file mode 160000 index 000000000..31d9704fd --- /dev/null +++ b/externals/lodepng/lodepng @@ -0,0 +1 @@ +Subproject commit 31d9704fdcca0b68fb9656d4764fa0fb60e460c2 diff --git a/src/citra/CMakeLists.txt b/src/citra/CMakeLists.txt index 1fac295f7..86066dfe9 100644 --- a/src/citra/CMakeLists.txt +++ b/src/citra/CMakeLists.txt @@ -8,13 +8,15 @@ add_executable(citra default_ini.h emu_window/emu_window_sdl2.cpp emu_window/emu_window_sdl2.h + lodepng_image_interface.cpp + lodepng_image_interface.h resource.h ) create_target_directory_groups(citra) target_link_libraries(citra PRIVATE common core input_common network) -target_link_libraries(citra PRIVATE inih glad) +target_link_libraries(citra PRIVATE inih glad lodepng) if (MSVC) target_link_libraries(citra PRIVATE getopt) endif() diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index c9a6127f7..294ba7b88 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -20,6 +20,7 @@ #include "citra/config.h" #include "citra/emu_window/emu_window_sdl2.h" +#include "citra/lodepng_image_interface.h" #include "common/common_paths.h" #include "common/detached_tasks.h" #include "common/file_util.h" @@ -342,6 +343,9 @@ int main(int argc, char** argv) { // Register frontend applets Frontend::RegisterDefaultApplets(); + // Register generic image interface + Core::System::GetInstance().RegisterImageInterface(std::make_shared()); + std::unique_ptr emu_window{std::make_unique(fullscreen)}; Core::System& system{Core::System::GetInstance()}; diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 810588299..1b18f4536 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -162,6 +162,12 @@ void Config::ReadValues() { Settings::values.custom_bottom_bottom = static_cast(sdl2_config->GetInteger("Layout", "custom_bottom_bottom", 480)); + // Utility + Settings::values.dump_textures = sdl2_config->GetBoolean("Utility", "dump_textures", false); + Settings::values.custom_textures = sdl2_config->GetBoolean("Utility", "custom_textures", false); + Settings::values.preload_textures = + sdl2_config->GetBoolean("Utility", "preload_textures", false); + // Audio Settings::values.enable_dsp_lle = sdl2_config->GetBoolean("Audio", "enable_dsp_lle", false); Settings::values.enable_dsp_lle_multithread = diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 29c2f723a..2b6aa2147 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -178,6 +178,18 @@ custom_bottom_bottom = # 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent swap_screen = +# Dumps textures as PNG to dump/textures/[Title ID]/. +# 0 (default): Off, 1: On +dump_textures = + +# Reads PNG files from load/textures/[Title ID]/ and replaces textures. +# 0 (default): Off, 1: On +custom_textures = + +# Loads all custom textures into memory before booting. +# 0 (default): Off, 1: On +preload_textures = + [Audio] # Whether or not to enable DSP LLE # 0 (default): No, 1: Yes diff --git a/src/citra/lodepng_image_interface.cpp b/src/citra/lodepng_image_interface.cpp new file mode 100644 index 000000000..e537de4e4 --- /dev/null +++ b/src/citra/lodepng_image_interface.cpp @@ -0,0 +1,29 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "citra/lodepng_image_interface.h" +#include "common/logging/log.h" + +bool LodePNGImageInterface::DecodePNG(std::vector& dst, u32& width, u32& height, + const std::string& path) { + u32 lodepng_ret = lodepng::decode(dst, width, height, path); + if (lodepng_ret) { + LOG_CRITICAL(Frontend, "Failed to decode {} because {}", path, + lodepng_error_text(lodepng_ret)); + return false; + } + return true; +} + +bool LodePNGImageInterface::EncodePNG(const std::string& path, const std::vector& src, + u32 width, u32 height) { + u32 lodepng_ret = lodepng::encode(path, src, width, height); + if (lodepng_ret) { + LOG_CRITICAL(Frontend, "Failed to encode {} because {}", path, + lodepng_error_text(lodepng_ret)); + return false; + } + return true; +} diff --git a/src/citra/lodepng_image_interface.h b/src/citra/lodepng_image_interface.h new file mode 100644 index 000000000..6880b10a0 --- /dev/null +++ b/src/citra/lodepng_image_interface.h @@ -0,0 +1,14 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/frontend/image_interface.h" + +class LodePNGImageInterface final : public Frontend::ImageInterface { +public: + bool DecodePNG(std::vector& dst, u32& width, u32& height, const std::string& path) override; + bool EncodePNG(const std::string& path, const std::vector& src, u32 width, + u32 height) override; +}; diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 0198ada76..c75754fce 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -47,6 +47,8 @@ add_executable(citra-qt configuration/configure_debug.cpp configuration/configure_debug.h configuration/configure_debug.ui + configuration/configure_enhancements.cpp + configuration/configure_enhancements.h configuration/configure_dialog.cpp configuration/configure_dialog.h configuration/configure_general.cpp @@ -142,6 +144,8 @@ add_executable(citra-qt multiplayer/validation.h uisettings.cpp uisettings.h + qt_image_interface.cpp + qt_image_interface.h updater/updater.cpp updater/updater.h updater/updater_p.h diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 96847c0b4..feed246b4 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -56,7 +56,7 @@ const std::array, Settings::NativeAnalog::NumAnalogs> Config: // This must be in alphabetical order according to action name as it must have the same order as // UISetting::values.shortcuts, which is alphabetically ordered. // clang-format off -const std::array default_hotkeys{ +const std::array default_hotkeys{ {{QStringLiteral("Advance Frame"), QStringLiteral("Main Window"), {QStringLiteral("\\"), Qt::ApplicationShortcut}}, {QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::ApplicationShortcut}}, {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}}, @@ -75,7 +75,8 @@ const std::array default_hotkeys{ {QStringLiteral("Toggle Frame Advancing"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+A"), Qt::ApplicationShortcut}}, {QStringLiteral("Toggle Screen Layout"), QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::WindowShortcut}}, {QStringLiteral("Toggle Speed Limit"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+Z"), Qt::ApplicationShortcut}}, - {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("Ctrl+D"), Qt::ApplicationShortcut}}}}; // clang-format on void Config::ReadValues() { @@ -230,6 +231,16 @@ void Config::ReadControlValues() { qt_config->endGroup(); } +void Config::ReadUtilityValues() { + qt_config->beginGroup("Utility"); + + Settings::values.dump_textures = ReadSetting("dump_textures", false).toBool(); + Settings::values.custom_textures = ReadSetting("custom_textures", false).toBool(); + Settings::values.preload_textures = ReadSetting("preload_textures", false).toBool(); + + qt_config->endGroup(); +} + void Config::ReadCoreValues() { qt_config->beginGroup(QStringLiteral("Core")); @@ -693,6 +704,16 @@ void Config::SaveControlValues() { qt_config->endGroup(); } +void Config::SaveUtilityValues() { + qt_config->beginGroup("Utility"); + + WriteSetting("dump_textures", Settings::values.dump_textures, false); + WriteSetting("custom_textures", Settings::values.custom_textures, false); + WriteSetting("preload_textures", Settings::values.preload_textures, false); + + qt_config->endGroup(); +} + void Config::SaveCoreValues() { qt_config->beginGroup(QStringLiteral("Core")); diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 08df67ead..7d65d41b6 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -42,6 +42,7 @@ private: void ReadUIGameListValues(); void ReadUILayoutValues(); void ReadUpdaterValues(); + void ReadUtilityValues(); void ReadWebServiceValues(); void SaveValues(); @@ -62,6 +63,7 @@ private: void SaveUIGameListValues(); void SaveUILayoutValues(); void SaveUpdaterValues(); + void SaveUtilityValues(); void SaveWebServiceValues(); QVariant ReadSetting(const QString& name) const; diff --git a/src/citra_qt/configuration/configure.ui b/src/citra_qt/configuration/configure.ui index 8b625d8c3..efe53606e 100644 --- a/src/citra_qt/configuration/configure.ui +++ b/src/citra_qt/configuration/configure.ui @@ -48,6 +48,11 @@ Graphics + + + Enhancements + + Audio @@ -135,6 +140,12 @@
configuration/configure_graphics.h
1 + + ConfigureEnhancements + QWidget +
configuration/configure_enhancements.h
+ 1 +
ConfigureWeb QWidget diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp index e8815d760..9c1214917 100644 --- a/src/citra_qt/configuration/configure_dialog.cpp +++ b/src/citra_qt/configuration/configure_dialog.cpp @@ -45,6 +45,7 @@ void ConfigureDialog::SetConfiguration() { ui->systemTab->SetConfiguration(); ui->inputTab->LoadConfiguration(); ui->graphicsTab->SetConfiguration(); + ui->enhancementsTab->SetConfiguration(); ui->audioTab->SetConfiguration(); ui->cameraTab->SetConfiguration(); ui->debugTab->SetConfiguration(); @@ -59,6 +60,7 @@ void ConfigureDialog::ApplyConfiguration() { ui->inputTab->ApplyProfile(); ui->hotkeysTab->ApplyConfiguration(registry); ui->graphicsTab->ApplyConfiguration(); + ui->enhancementsTab->ApplyConfiguration(); ui->audioTab->ApplyConfiguration(); ui->cameraTab->ApplyConfiguration(); ui->debugTab->ApplyConfiguration(); @@ -76,7 +78,7 @@ void ConfigureDialog::PopulateSelectionList() { const std::array>, 4> items{ {{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->uiTab}}, {tr("System"), {ui->systemTab, ui->audioTab, ui->cameraTab}}, - {tr("Graphics"), {ui->graphicsTab}}, + {tr("Graphics"), {ui->enhancementsTab, ui->graphicsTab}}, {tr("Controls"), {ui->inputTab, ui->hotkeysTab}}}}; for (const auto& entry : items) { @@ -109,6 +111,7 @@ void ConfigureDialog::RetranslateUI() { ui->inputTab->RetranslateUI(); ui->hotkeysTab->RetranslateUI(); ui->graphicsTab->RetranslateUI(); + ui->enhancementsTab->RetranslateUI(); ui->audioTab->RetranslateUI(); ui->cameraTab->RetranslateUI(); ui->debugTab->RetranslateUI(); @@ -121,12 +124,17 @@ void ConfigureDialog::UpdateVisibleTabs() { if (items.isEmpty()) return; - const std::map widgets = { - {ui->generalTab, tr("General")}, {ui->systemTab, tr("System")}, - {ui->inputTab, tr("Input")}, {ui->hotkeysTab, tr("Hotkeys")}, - {ui->graphicsTab, tr("Graphics")}, {ui->audioTab, tr("Audio")}, - {ui->cameraTab, tr("Camera")}, {ui->debugTab, tr("Debug")}, - {ui->webTab, tr("Web")}, {ui->uiTab, tr("UI")}}; + const std::map widgets = {{ui->generalTab, tr("General")}, + {ui->systemTab, tr("System")}, + {ui->inputTab, tr("Input")}, + {ui->hotkeysTab, tr("Hotkeys")}, + {ui->enhancementsTab, tr("Enhancements")}, + {ui->graphicsTab, tr("Advanced")}, + {ui->audioTab, tr("Audio")}, + {ui->cameraTab, tr("Camera")}, + {ui->debugTab, tr("Debug")}, + {ui->webTab, tr("Web")}, + {ui->uiTab, tr("UI")}}; ui->tabWidget->clear(); diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp new file mode 100644 index 000000000..677f868c2 --- /dev/null +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -0,0 +1,110 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "citra_qt/configuration/configure_enhancements.h" +#include "core/core.h" +#include "core/settings.h" +#include "ui_configure_enhancements.h" +#include "video_core/renderer_opengl/post_processing_opengl.h" + +ConfigureEnhancements::ConfigureEnhancements(QWidget* parent) + : QWidget(parent), ui(new Ui::ConfigureEnhancements) { + ui->setupUi(this); + SetConfiguration(); + + ui->layoutBox->setEnabled(!Settings::values.custom_layout); + + ui->resolution_factor_combobox->setEnabled(Settings::values.use_hw_renderer); + + connect(ui->render_3d_combobox, + static_cast(&QComboBox::currentIndexChanged), this, + [this](int currentIndex) { + updateShaders(static_cast(currentIndex) == + Settings::StereoRenderOption::Anaglyph); + }); + + connect(ui->bg_button, &QPushButton::clicked, this, [this] { + const QColor new_bg_color = QColorDialog::getColor(bg_color); + if (!new_bg_color.isValid()) { + return; + } + bg_color = new_bg_color; + QPixmap pixmap(ui->bg_button->size()); + pixmap.fill(bg_color); + const QIcon color_icon(pixmap); + ui->bg_button->setIcon(color_icon); + }); + + ui->toggle_preload_textures->setEnabled(ui->toggle_custom_textures->isChecked()); + connect(ui->toggle_custom_textures, &QCheckBox::toggled, this, [this] { + ui->toggle_preload_textures->setEnabled(ui->toggle_custom_textures->isChecked()); + if (!ui->toggle_preload_textures->isEnabled()) + ui->toggle_preload_textures->setChecked(false); + }); +} + +void ConfigureEnhancements::SetConfiguration() { + ui->resolution_factor_combobox->setCurrentIndex(Settings::values.resolution_factor); + ui->render_3d_combobox->setCurrentIndex(static_cast(Settings::values.render_3d)); + ui->factor_3d->setValue(Settings::values.factor_3d); + updateShaders(Settings::values.render_3d == Settings::StereoRenderOption::Anaglyph); + ui->toggle_linear_filter->setChecked(Settings::values.filter_mode); + ui->layout_combobox->setCurrentIndex(static_cast(Settings::values.layout_option)); + ui->swap_screen->setChecked(Settings::values.swap_screen); + ui->toggle_dump_textures->setChecked(Settings::values.dump_textures); + ui->toggle_custom_textures->setChecked(Settings::values.custom_textures); + ui->toggle_preload_textures->setChecked(Settings::values.preload_textures); + bg_color = QColor::fromRgbF(Settings::values.bg_red, Settings::values.bg_green, + Settings::values.bg_blue); + QPixmap pixmap(ui->bg_button->size()); + pixmap.fill(bg_color); + const QIcon color_icon(pixmap); + ui->bg_button->setIcon(color_icon); +} + +void ConfigureEnhancements::updateShaders(bool anaglyph) { + ui->shader_combobox->clear(); + + if (anaglyph) + ui->shader_combobox->addItem("dubois (builtin)"); + else + ui->shader_combobox->addItem("none (builtin)"); + + ui->shader_combobox->setCurrentIndex(0); + + for (const auto& shader : OpenGL::GetPostProcessingShaderList(anaglyph)) { + ui->shader_combobox->addItem(QString::fromStdString(shader)); + if (Settings::values.pp_shader_name == shader) + ui->shader_combobox->setCurrentIndex(ui->shader_combobox->count() - 1); + } +} + +void ConfigureEnhancements::RetranslateUI() { + ui->retranslateUi(this); +} + +void ConfigureEnhancements::ApplyConfiguration() { + Settings::values.resolution_factor = + static_cast(ui->resolution_factor_combobox->currentIndex()); + Settings::values.render_3d = + static_cast(ui->render_3d_combobox->currentIndex()); + Settings::values.factor_3d = ui->factor_3d->value(); + Settings::values.pp_shader_name = + ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString(); + Settings::values.filter_mode = ui->toggle_linear_filter->isChecked(); + Settings::values.layout_option = + static_cast(ui->layout_combobox->currentIndex()); + Settings::values.swap_screen = ui->swap_screen->isChecked(); + Settings::values.dump_textures = ui->toggle_dump_textures->isChecked(); + Settings::values.custom_textures = ui->toggle_custom_textures->isChecked(); + Settings::values.preload_textures = ui->toggle_preload_textures->isChecked(); + Settings::values.bg_red = static_cast(bg_color.redF()); + Settings::values.bg_green = static_cast(bg_color.greenF()); + Settings::values.bg_blue = static_cast(bg_color.blueF()); +} + +ConfigureEnhancements::~ConfigureEnhancements() { + delete ui; +} diff --git a/src/citra_qt/configuration/configure_enhancements.h b/src/citra_qt/configuration/configure_enhancements.h new file mode 100644 index 000000000..422c1dc35 --- /dev/null +++ b/src/citra_qt/configuration/configure_enhancements.h @@ -0,0 +1,29 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +namespace Ui { +class ConfigureEnhancements; +} + +class ConfigureEnhancements : public QWidget { + Q_OBJECT + +public: + explicit ConfigureEnhancements(QWidget* parent = nullptr); + ~ConfigureEnhancements(); + + void ApplyConfiguration(); + void RetranslateUI(); + void SetConfiguration(); + +private: + void updateShaders(bool anaglyph); + + Ui::ConfigureEnhancements* ui; + QColor bg_color; +}; diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui new file mode 100644 index 000000000..bb677dabb --- /dev/null +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -0,0 +1,317 @@ + + + ConfigureEnhancements + + + + 0 + 0 + 400 + 595 + + + + + 0 + 0 + + + + Form + + + + + + Renderer + + + + + + + + Internal Resolution + + + + + + + + Auto (Window Size) + + + + + Native (400x240) + + + + + 2x Native (800x480) + + + + + 3x Native (1200x720) + + + + + 4x Native (1600x960) + + + + + 5x Native (2000x1200) + + + + + 6x Native (2400x1440) + + + + + 7x Native (2800x1680) + + + + + 8x Native (3200x1920) + + + + + 9x Native (3600x2160) + + + + + 10x Native (4000x2400) + + + + + + + + + + Enable Linear Filtering + + + + + + + + + Post-Processing Shader + + + + + + + + + + + + + + + Stereoscopy + + + + + + + + Stereoscopic 3D Mode + + + + + + + + Off + + + + + Side by Side + + + + + Anaglyph + + + + + + + + + + + + Depth + + + + + + + % + + + 0 + + + 100 + + + 0 + + + + + + + + + + + + Layout + + + + + + + + Screen Layout: + + + + + + + + Default + + + + + Single Screen + + + + + Large Screen + + + + + Side by Side + + + + + + + + + + Swap Screens + + + + + + + + + Background Color: + + + + + + + + 40 + 16777215 + + + + + + + + + + + + + Utility + + + + + + <html><head/><body><p>Replace textures with PNG files.</p><p>Textures are loaded from load/textures/[Title ID]/.</p></body></html> + + + Use Custom Textures + + + + + + + <html><head/><body><p>Dump textures to PNG files.</p><p>Textures are dumped to dump/textures/[Title ID]/.</p></body></html> + + + Dump Textures + + + + + + + <html><head/><body><p>Load all custom textures into memory on boot, instead of loading them when the game requires them.</p></body></html> + + + Preload Custom Textures + + + + + + + + + + Qt::Vertical + + + + 20 + 165 + + + + + + + + + diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index 47d559e3d..e4b6be073 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -25,6 +25,10 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) ConfigureGeneral::~ConfigureGeneral() = default; void ConfigureGeneral::SetConfiguration() { + ui->toggle_frame_limit->setChecked(Settings::values.use_frame_limit); + ui->frame_limit->setEnabled(ui->toggle_frame_limit->isChecked()); + ui->frame_limit->setValue(Settings::values.frame_limit); + ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); ui->toggle_background_pause->setChecked(UISettings::values.pause_when_in_background); @@ -53,6 +57,9 @@ void ConfigureGeneral::ResetDefaults() { } void ConfigureGeneral::ApplyConfiguration() { + Settings::values.use_frame_limit = ui->toggle_frame_limit->isChecked(); + Settings::values.frame_limit = ui->frame_limit->value(); + UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); UISettings::values.pause_when_in_background = ui->toggle_background_pause->isChecked(); diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/citra_qt/configuration/configure_graphics.cpp index 8edcdb491..d04c0d6fe 100644 --- a/src/citra_qt/configuration/configure_graphics.cpp +++ b/src/citra_qt/configuration/configure_graphics.cpp @@ -17,11 +17,12 @@ ConfigureGraphics::ConfigureGraphics(QWidget* parent) ui->setupUi(this); SetConfiguration(); - ui->layoutBox->setEnabled(!Settings::values.custom_layout); - ui->hw_renderer_group->setEnabled(ui->toggle_hw_renderer->isChecked()); - connect(ui->toggle_hw_renderer, &QCheckBox::toggled, ui->hw_renderer_group, - &QWidget::setEnabled); + connect(ui->toggle_hw_renderer, &QCheckBox::toggled, this, [this] { + auto checked = ui->toggle_hw_renderer->isChecked(); + ui->hw_renderer_group->setEnabled(checked); + }); + ui->hw_shader_group->setEnabled(ui->toggle_hw_shader->isChecked()); connect(ui->toggle_hw_shader, &QCheckBox::toggled, ui->hw_shader_group, &QWidget::setEnabled); #ifdef __APPLE__ @@ -36,21 +37,6 @@ ConfigureGraphics::ConfigureGraphics(QWidget* parent) } }); #endif - - connect(ui->render_3d_combobox, - static_cast(&QComboBox::currentIndexChanged), this, - [this](int currentIndex) { - updateShaders(static_cast(currentIndex) == - Settings::StereoRenderOption::Anaglyph); - }); - - connect(ui->bg_button, &QPushButton::clicked, this, [this] { - const QColor new_bg_color = QColorDialog::getColor(bg_color); - if (!new_bg_color.isValid()) { - return; - } - UpdateBackgroundColorButton(new_bg_color); - }); } ConfigureGraphics::~ConfigureGraphics() = default; @@ -60,15 +46,6 @@ void ConfigureGraphics::SetConfiguration() { ui->toggle_hw_shader->setChecked(Settings::values.use_hw_shader); ui->toggle_accurate_mul->setChecked(Settings::values.shaders_accurate_mul); ui->toggle_shader_jit->setChecked(Settings::values.use_shader_jit); - ui->resolution_factor_combobox->setCurrentIndex(Settings::values.resolution_factor); - ui->render_3d_combobox->setCurrentIndex(static_cast(Settings::values.render_3d)); - ui->factor_3d->setValue(Settings::values.factor_3d); - updateShaders(Settings::values.render_3d == Settings::StereoRenderOption::Anaglyph); - ui->toggle_linear_filter->setChecked(Settings::values.filter_mode); - ui->layout_combobox->setCurrentIndex(static_cast(Settings::values.layout_option)); - ui->swap_screen->setChecked(Settings::values.swap_screen); - UpdateBackgroundColorButton(QColor::fromRgbF(Settings::values.bg_red, Settings::values.bg_green, - Settings::values.bg_blue)); } void ConfigureGraphics::ApplyConfiguration() { @@ -76,47 +53,6 @@ void ConfigureGraphics::ApplyConfiguration() { Settings::values.use_hw_shader = ui->toggle_hw_shader->isChecked(); Settings::values.shaders_accurate_mul = ui->toggle_accurate_mul->isChecked(); Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked(); - Settings::values.resolution_factor = - static_cast(ui->resolution_factor_combobox->currentIndex()); - Settings::values.render_3d = - static_cast(ui->render_3d_combobox->currentIndex()); - Settings::values.factor_3d = ui->factor_3d->value(); - Settings::values.pp_shader_name = - ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString(); - Settings::values.filter_mode = ui->toggle_linear_filter->isChecked(); - Settings::values.layout_option = - static_cast(ui->layout_combobox->currentIndex()); - Settings::values.swap_screen = ui->swap_screen->isChecked(); - Settings::values.bg_red = static_cast(bg_color.redF()); - Settings::values.bg_green = static_cast(bg_color.greenF()); - Settings::values.bg_blue = static_cast(bg_color.blueF()); -} - -void ConfigureGraphics::updateShaders(bool anaglyph) { - ui->shader_combobox->clear(); - - if (anaglyph) - ui->shader_combobox->addItem("dubois (builtin)"); - else - ui->shader_combobox->addItem("none (builtin)"); - - ui->shader_combobox->setCurrentIndex(0); - - for (const auto& shader : OpenGL::GetPostProcessingShaderList(anaglyph)) { - ui->shader_combobox->addItem(QString::fromStdString(shader)); - if (Settings::values.pp_shader_name == shader) - ui->shader_combobox->setCurrentIndex(ui->shader_combobox->count() - 1); - } -} - -void ConfigureGraphics::UpdateBackgroundColorButton(const QColor& color) { - bg_color = color; - - QPixmap pixmap(ui->bg_button->size()); - pixmap.fill(bg_color); - - const QIcon color_icon(pixmap); - ui->bg_button->setIcon(color_icon); } void ConfigureGraphics::RetranslateUI() { diff --git a/src/citra_qt/configuration/configure_graphics.h b/src/citra_qt/configuration/configure_graphics.h index e3e6c6a18..9ed932090 100644 --- a/src/citra_qt/configuration/configure_graphics.h +++ b/src/citra_qt/configuration/configure_graphics.h @@ -26,7 +26,4 @@ public: std::unique_ptr ui; QColor bg_color; - -private: - void updateShaders(bool anaglyph); }; diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/citra_qt/configuration/configure_graphics.ui index b1e33cb0e..88b9a0caa 100644 --- a/src/citra_qt/configuration/configure_graphics.ui +++ b/src/citra_qt/configuration/configure_graphics.ui @@ -7,9 +7,15 @@ 0 0 400 - 603 + 430 + + + 0 + 0 + + Form @@ -45,76 +51,6 @@ 0 - - - - - - Internal Resolution - - - - - - - - Auto (Window Size) - - - - - Native (400x240) - - - - - 2x Native (800x480) - - - - - 3x Native (1200x720) - - - - - 4x Native (1600x960) - - - - - 5x Native (2000x1200) - - - - - 6x Native (2400x1440) - - - - - 7x Native (2800x1680) - - - - - 8x Native (3200x1920) - - - - - 9x Native (3600x2160) - - - - - 10x Native (4000x2400) - - - - - - @@ -166,165 +102,6 @@ - - - - Enable Linear Filtering - - - - - - - - - Post-Processing Shader - - - - - - - - - -
- - - - - Stereoscopy - - - - - - - - Stereoscopic 3D Mode - - - - - - - - Off - - - - - Side by Side - - - - - Anaglyph - - - - - - - - - - - - Depth - - - - - - - % - - - 0 - - - 100 - - - 0 - - - - - - - - - - - - Layout - - - - - - - - Screen Layout: - - - - - - - - Default - - - - - Single Screen - - - - - Large Screen - - - - - Side by Side - - - - - - - - - - Swap Screens - - - - - - - - - Background Color: - - - - - - - - 40 - 16777215 - - - - - - @@ -336,7 +113,7 @@ 20 - 40 + 270 diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index d137d8472..8a3b992f1 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -458,6 +458,9 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra QAction* open_extdata_location = context_menu.addAction(tr("Open Extra Data Location")); QAction* open_application_location = context_menu.addAction(tr("Open Application Location")); QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location")); + QAction* open_texture_dump_location = context_menu.addAction(tr("Open Texture Dump Location")); + QAction* open_texture_load_location = + context_menu.addAction(tr("Open Custom Texture Location")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); const bool is_application = @@ -484,6 +487,10 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra program_id + 0xe00000000) + "content/")); auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + open_texture_dump_location->setVisible(is_application); + open_texture_load_location->setVisible(is_application); + navigate_to_gamedb_entry->setVisible(it != compatibility_list.end()); connect(open_save_location, &QAction::triggered, [this, program_id] { @@ -498,6 +505,20 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra connect(open_update_location, &QAction::triggered, [this, program_id] { emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA); }); + connect(open_texture_dump_location, &QAction::triggered, [this, program_id] { + if (FileUtil::CreateFullPath(fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), + program_id))) { + emit OpenFolderRequested(program_id, GameListOpenTarget::TEXTURE_DUMP); + } + }); + connect(open_texture_load_location, &QAction::triggered, [this, program_id] { + if (FileUtil::CreateFullPath(fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), + program_id))) { + emit OpenFolderRequested(program_id, GameListOpenTarget::TEXTURE_LOAD); + } + }); connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index bef23f31b..b98e79b57 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -29,7 +29,14 @@ class QTreeView; class QToolButton; class QVBoxLayout; -enum class GameListOpenTarget { SAVE_DATA = 0, EXT_DATA = 1, APPLICATION = 2, UPDATE_DATA = 3 }; +enum class GameListOpenTarget { + SAVE_DATA = 0, + EXT_DATA = 1, + APPLICATION = 2, + UPDATE_DATA = 3, + TEXTURE_DUMP = 4, + TEXTURE_LOAD = 5 +}; class GameList : public QWidget { Q_OBJECT diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 78836be23..11da2be20 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -50,6 +50,7 @@ #include "citra_qt/hotkeys.h" #include "citra_qt/main.h" #include "citra_qt/multiplayer/state.h" +#include "citra_qt/qt_image_interface.h" #include "citra_qt/uisettings.h" #include "citra_qt/updater/updater.h" #include "citra_qt/util/clickable_label.h" @@ -427,8 +428,11 @@ void GMainWindow::InitializeHotkeys() { Settings::values.use_frame_limit = !Settings::values.use_frame_limit; UpdateStatusBar(); }); - // 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 + connect(hotkey_registry.GetHotkey("Main Window", "Toggle Texture Dumping", this), + &QShortcut::activated, this, + [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes + // the variable hold a garbage value after this function exits static constexpr u16 SPEED_LIMIT_STEP = 5; connect(hotkey_registry.GetHotkey("Main Window", "Increase Speed Limit", this), &QShortcut::activated, this, [&] { @@ -1080,6 +1084,16 @@ void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + "content/"; break; + case GameListOpenTarget::TEXTURE_DUMP: + open_target = "Dumped Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id); + break; + case GameListOpenTarget::TEXTURE_LOAD: + open_target = "Custom Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); + break; default: LOG_ERROR(Frontend, "Unexpected target {}", static_cast(target)); return; @@ -2059,6 +2073,9 @@ int main(int argc, char* argv[]) { Core::System::GetInstance().RegisterMiiSelector(std::make_shared(main_window)); Core::System::GetInstance().RegisterSoftwareKeyboard(std::make_shared(main_window)); + // Register Qt image interface + Core::System::GetInstance().RegisterImageInterface(std::make_shared()); + main_window.show(); QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, diff --git a/src/citra_qt/qt_image_interface.cpp b/src/citra_qt/qt_image_interface.cpp new file mode 100644 index 000000000..00a5e1384 --- /dev/null +++ b/src/citra_qt/qt_image_interface.cpp @@ -0,0 +1,38 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "citra_qt/qt_image_interface.h" +#include "common/logging/log.h" + +bool QtImageInterface::DecodePNG(std::vector& dst, u32& width, u32& height, + const std::string& path) { + QImage image(QString::fromStdString(path)); + + if (image.isNull()) { + LOG_ERROR(Frontend, "Failed to open {} for decoding", path); + return false; + } + width = image.width(); + height = image.height(); + + image = image.convertToFormat(QImage::Format_RGBA8888); + + // Write RGBA8 to vector + dst = std::vector(image.constBits(), image.constBits() + (width * height * 4)); + + return true; +} + +bool QtImageInterface::EncodePNG(const std::string& path, const std::vector& src, u32 width, + u32 height) { + QImage image(src.data(), width, height, QImage::Format_RGBA8888); + + if (!image.save(QString::fromStdString(path), "PNG")) { + LOG_ERROR(Frontend, "Failed to save {}", path); + return false; + } + return true; +} diff --git a/src/citra_qt/qt_image_interface.h b/src/citra_qt/qt_image_interface.h new file mode 100644 index 000000000..53b49b7b8 --- /dev/null +++ b/src/citra_qt/qt_image_interface.h @@ -0,0 +1,14 @@ +// 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 QtImageInterface final : public Frontend::ImageInterface { +public: + bool DecodePNG(std::vector& dst, u32& width, u32& height, const std::string& path) override; + bool EncodePNG(const std::string& path, const std::vector& src, u32 width, + u32 height) override; +}; diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index ba9706737..89ee75dd2 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -84,6 +84,8 @@ add_library(common STATIC swap.h telemetry.cpp telemetry.h + texture.cpp + texture.h thread.cpp thread.h thread_queue_list.h diff --git a/src/common/common_paths.h b/src/common/common_paths.h index c5d6ac67b..13e71615e 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -44,6 +44,9 @@ #define CHEATS_DIR "cheats" #define DLL_DIR "external_dlls" #define SHADER_DIR "shaders" +#define DUMP_DIR "dump" +#define LOAD_DIR "load" +#define SHADER_DIR "shaders" // Filenames // Files in the directory returned by GetUserPath(UserPath::LogDir) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 0e9a9637c..249fbbb6f 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -469,6 +469,17 @@ u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry, return ForeachDirectoryEntry(&num_entries, directory, callback) ? num_entries : 0; } +void GetAllFilesFromNestedEntries(FSTEntry& directory, std::vector& output) { + std::vector files; + for (auto& entry : directory.children) { + if (entry.isDirectory) { + GetAllFilesFromNestedEntries(entry, output); + } else { + output.push_back(entry); + } + } +} + bool DeleteDirRecursively(const std::string& directory, unsigned int recursion) { const auto callback = [recursion](u64* num_entries_out, const std::string& directory, const std::string& virtual_name) -> bool { @@ -712,6 +723,8 @@ void SetUserPath(const std::string& path) { g_paths.emplace(UserPath::CheatsDir, user_path + CHEATS_DIR DIR_SEP); g_paths.emplace(UserPath::DLLDir, user_path + DLL_DIR DIR_SEP); g_paths.emplace(UserPath::ShaderDir, user_path + SHADER_DIR DIR_SEP); + g_paths.emplace(UserPath::DumpDir, user_path + DUMP_DIR DIR_SEP); + g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP); } const std::string& GetUserPath(UserPath path) { diff --git a/src/common/file_util.h b/src/common/file_util.h index 28e6a42d4..45917423a 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -26,6 +26,8 @@ enum class UserPath { CheatsDir, ConfigDir, DLLDir, + DumpDir, + LoadDir, LogDir, NANDDir, RootDir, @@ -113,6 +115,13 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry, unsigned int recursion = 0); +/** + * Recursively searches through a FSTEntry for files, and stores them. + * @param directory The FSTEntry to start scanning from + * @param parent_entry FSTEntry vector where the results will be stored. + */ +void GetAllFilesFromNestedEntries(FSTEntry& directory, std::vector& output); + // deletes the given directory and anything under it. Returns true on success. bool DeleteDirRecursively(const std::string& directory, unsigned int recursion = 256); diff --git a/src/common/texture.cpp b/src/common/texture.cpp new file mode 100644 index 000000000..2ec939288 --- /dev/null +++ b/src/common/texture.cpp @@ -0,0 +1,22 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/assert.h" +#include "common/common_types.h" + +namespace Common { +void FlipRGBA8Texture(std::vector& tex, u64 width, u64 height) { + ASSERT(tex.size() == width * height * 4); + const u64 line_size = width * 4; + for (u64 line = 0; line < height / 2; line++) { + const u32 offset_1 = line * line_size; + const u32 offset_2 = (height - line - 1) * line_size; + // Swap lines + std::swap_ranges(tex.begin() + offset_1, tex.begin() + offset_1 + line_size, + tex.begin() + offset_2); + } +} +} // namespace Common diff --git a/src/common/texture.h b/src/common/texture.h new file mode 100644 index 000000000..2de56bc09 --- /dev/null +++ b/src/common/texture.h @@ -0,0 +1,12 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/common_types.h" + +namespace Common { +void FlipRGBA8Texture(std::vector& tex, u64 width, u64 height); +} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5ec409f1e..3a27d8f81 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -36,6 +36,8 @@ add_library(core STATIC core.h core_timing.cpp core_timing.h + custom_tex_cache.cpp + custom_tex_cache.h dumping/backend.cpp dumping/backend.h file_sys/archive_backend.cpp @@ -100,6 +102,7 @@ add_library(core STATIC frontend/emu_window.h frontend/framebuffer_layout.cpp frontend/framebuffer_layout.h + frontend/image_interface.h frontend/input.h frontend/mic.h frontend/mic.cpp diff --git a/src/core/core.cpp b/src/core/core.cpp index 03dfc59c9..c0dc93b55 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -8,6 +8,7 @@ #include "audio_core/hle/hle.h" #include "audio_core/lle/lle.h" #include "common/logging/log.h" +#include "common/texture.h" #include "core/arm/arm_interface.h" #ifdef ARCHITECTURE_x86_64 #include "core/arm/dynarmic/arm_dynarmic.h" @@ -20,6 +21,7 @@ #ifdef ENABLE_FFMPEG_VIDEO_DUMPER #include "core/dumping/ffmpeg_backend.h" #endif +#include "core/custom_tex_cache.h" #include "core/gdbstub/gdbstub.h" #include "core/hle/kernel/client_port.h" #include "core/hle/kernel/kernel.h" @@ -152,6 +154,15 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st static_cast(load_result)); } perf_stats = std::make_unique(title_id); + custom_tex_cache = std::make_unique(); + if (Settings::values.custom_textures) { + FileUtil::CreateFullPath(fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), + Kernel().GetCurrentProcess()->codeset->program_id)); + custom_tex_cache->FindCustomTextures(); + } + if (Settings::values.preload_textures) + custom_tex_cache->PreloadTextures(); status = ResultStatus::Success; m_emu_window = &emu_window; m_filepath = filepath; @@ -298,6 +309,14 @@ const VideoDumper::Backend& System::VideoDumper() const { return *video_dumper; } +Core::CustomTexCache& System::CustomTexCache() { + return *custom_tex_cache; +} + +const Core::CustomTexCache& System::CustomTexCache() const { + return *custom_tex_cache; +} + void System::RegisterMiiSelector(std::shared_ptr mii_selector) { registered_mii_selector = std::move(mii_selector); } @@ -306,6 +325,10 @@ void System::RegisterSoftwareKeyboard(std::shared_ptr image_interface) { + registered_image_interface = std::move(image_interface); +} + void System::Shutdown() { // Log last frame performance stats const auto perf_results = GetAndResetPerfStats(); diff --git a/src/core/core.h b/src/core/core.h index d4747ae36..b533c4bc3 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -7,8 +7,10 @@ #include #include #include "common/common_types.h" +#include "core/custom_tex_cache.h" #include "core/frontend/applets/mii_selector.h" #include "core/frontend/applets/swkbd.h" +#include "core/frontend/image_interface.h" #include "core/loader/loader.h" #include "core/memory.h" #include "core/perf_stats.h" @@ -210,6 +212,15 @@ public: /// Gets a const reference to the cheat engine const Cheats::CheatEngine& CheatEngine() const; + /// Gets a reference to the custom texture cache system + Core::CustomTexCache& CustomTexCache(); + + /// Gets a const reference to the custom texture cache system + const Core::CustomTexCache& CustomTexCache() const; + + /// Handles loading all custom textures from disk into cache. + void PreloadCustomTextures(); + /// Gets a reference to the video dumper backend VideoDumper::Backend& VideoDumper(); @@ -248,6 +259,14 @@ public: return registered_swkbd; } + /// Image interface + + void RegisterImageInterface(std::shared_ptr image_interface); + + std::shared_ptr GetImageInterface() const { + return registered_image_interface; + } + private: /** * Initialize the emulated system. @@ -289,6 +308,12 @@ private: /// Video dumper backend std::unique_ptr video_dumper; + /// Custom texture cache system + std::unique_ptr custom_tex_cache; + + /// Image interface + std::shared_ptr registered_image_interface; + /// RPC Server for scripting support std::unique_ptr rpc_server; diff --git a/src/core/custom_tex_cache.cpp b/src/core/custom_tex_cache.cpp new file mode 100644 index 000000000..c878016f7 --- /dev/null +++ b/src/core/custom_tex_cache.cpp @@ -0,0 +1,111 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "common/file_util.h" +#include "common/texture.h" +#include "core.h" +#include "core/custom_tex_cache.h" + +namespace Core { +CustomTexCache::CustomTexCache() = default; + +CustomTexCache::~CustomTexCache() = default; + +bool CustomTexCache::IsTextureDumped(u64 hash) const { + return dumped_textures.count(hash); +} + +void CustomTexCache::SetTextureDumped(const u64 hash) { + dumped_textures.insert(hash); +} + +bool CustomTexCache::IsTextureCached(u64 hash) const { + return custom_textures.count(hash); +} + +const CustomTexInfo& CustomTexCache::LookupTexture(u64 hash) const { + return custom_textures.at(hash); +} + +void CustomTexCache::CacheTexture(u64 hash, const std::vector& tex, u32 width, u32 height) { + custom_textures[hash] = {width, height, tex}; +} + +void CustomTexCache::AddTexturePath(u64 hash, const std::string& path) { + if (custom_texture_paths.count(hash)) + LOG_ERROR(Core, "Textures {} and {} conflict!", custom_texture_paths[hash].path, path); + else + custom_texture_paths[hash] = {path, hash}; +} + +void CustomTexCache::FindCustomTextures() { + // 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), + Core::System::GetInstance().Kernel().GetCurrentProcess()->codeset->program_id); + + if (FileUtil::Exists(load_path)) { + FileUtil::FSTEntry texture_dir; + std::vector textures; + // 64 nested folders should be plenty for most cases + FileUtil::ScanDirectoryTree(load_path, texture_dir, 64); + FileUtil::GetAllFilesFromNestedEntries(texture_dir, textures); + + for (const auto& file : textures) { + if (file.isDirectory) + continue; + if (file.virtualName.substr(0, 5) != "tex1_") + continue; + + u32 width; + u32 height; + u64 hash; + u32 format; // unused + // TODO: more modern way of doing this + if (std::sscanf(file.virtualName.c_str(), "tex1_%ux%u_%llX_%u.png", &width, &height, + &hash, &format) == 4) { + AddTexturePath(hash, file.physicalName); + } + } + } +} + +void CustomTexCache::PreloadTextures() { + for (const auto& path : custom_texture_paths) { + const auto& image_interface = Core::System::GetInstance().GetImageInterface(); + const auto& path_info = path.second; + Core::CustomTexInfo tex_info; + if (image_interface->DecodePNG(tex_info.tex, tex_info.width, tex_info.height, + path_info.path)) { + // Make sure the texture size is a power of 2 + std::bitset<32> width_bits(tex_info.width); + std::bitset<32> height_bits(tex_info.height); + if (width_bits.count() == 1 && height_bits.count() == 1) { + LOG_DEBUG(Render_OpenGL, "Loaded custom texture from {}", path_info.path); + Common::FlipRGBA8Texture(tex_info.tex, tex_info.width, tex_info.height); + CacheTexture(path_info.hash, tex_info.tex, tex_info.width, tex_info.height); + } else { + LOG_ERROR(Render_OpenGL, "Texture {} size is not a power of 2", path_info.path); + } + } else { + LOG_ERROR(Render_OpenGL, "Failed to load custom texture {}", path_info.path); + } + } +} + +bool CustomTexCache::CustomTextureExists(u64 hash) const { + return custom_texture_paths.count(hash); +} + +const CustomTexPathInfo& CustomTexCache::LookupTexturePathInfo(u64 hash) const { + return custom_texture_paths.at(hash); +} + +bool CustomTexCache::IsTexturePathMapEmpty() const { + return custom_texture_paths.size() == 0; +} +} // namespace Core diff --git a/src/core/custom_tex_cache.h b/src/core/custom_tex_cache.h new file mode 100644 index 000000000..2e0e5dd65 --- /dev/null +++ b/src/core/custom_tex_cache.h @@ -0,0 +1,51 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" + +namespace Core { +struct CustomTexInfo { + u32 width; + u32 height; + std::vector tex; +}; + +// This is to avoid parsing the filename multiple times +struct CustomTexPathInfo { + std::string path; + u64 hash; +}; + +// TODO: think of a better name for this class... +class CustomTexCache { +public: + explicit CustomTexCache(); + ~CustomTexCache(); + + bool IsTextureDumped(u64 hash) const; + void SetTextureDumped(u64 hash); + + bool IsTextureCached(u64 hash) const; + const CustomTexInfo& LookupTexture(u64 hash) const; + void CacheTexture(u64 hash, const std::vector& tex, u32 width, u32 height); + + void AddTexturePath(u64 hash, const std::string& path); + void FindCustomTextures(); + void PreloadTextures(); + bool CustomTextureExists(u64 hash) const; + const CustomTexPathInfo& LookupTexturePathInfo(u64 hash) const; + bool IsTexturePathMapEmpty() const; + +private: + std::unordered_set dumped_textures; + std::unordered_map custom_textures; + std::unordered_map custom_texture_paths; +}; +} // namespace Core diff --git a/src/core/frontend/image_interface.h b/src/core/frontend/image_interface.h new file mode 100644 index 000000000..7febe3331 --- /dev/null +++ b/src/core/frontend/image_interface.h @@ -0,0 +1,24 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/common_types.h" + +namespace Frontend { + +class ImageInterface { +public: + virtual ~ImageInterface() = default; + + // Error logging should be handled by the frontend + virtual bool DecodePNG(std::vector& dst, u32& width, u32& height, + const std::string& path) = 0; + virtual bool EncodePNG(const std::string& path, const std::vector& src, u32 width, + u32 height) = 0; +}; + +} // namespace Frontend diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 2e0dccc03..079d276d0 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -87,6 +87,8 @@ void LogSettings() { LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d); LogSetting("Layout_LayoutOption", static_cast(Settings::values.layout_option)); LogSetting("Layout_SwapScreen", Settings::values.swap_screen); + LogSetting("Utility_DumpTextures", Settings::values.dump_textures); + LogSetting("Utility_CustomTextures", Settings::values.custom_textures); LogSetting("Audio_EnableDspLle", Settings::values.enable_dsp_lle); LogSetting("Audio_EnableDspLleMultithread", Settings::values.enable_dsp_lle_multithread); LogSetting("Audio_OutputEngine", Settings::values.sink_id); diff --git a/src/core/settings.h b/src/core/settings.h index 85fce121c..213cc3ec3 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -170,6 +170,10 @@ struct Values { bool filter_mode; std::string pp_shader_name; + bool dump_textures; + bool custom_textures; + bool preload_textures; + // Audio bool enable_dsp_lle; bool enable_dsp_lle_multithread; diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp index 1cce8dba8..af2a918a6 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp @@ -21,9 +21,14 @@ #include "common/math_util.h" #include "common/microprofile.h" #include "common/scope_exit.h" +#include "common/texture.h" #include "common/vector_math.h" +#include "core/core.h" +#include "core/custom_tex_cache.h" #include "core/frontend/emu_window.h" +#include "core/hle/kernel/process.h" #include "core/memory.h" +#include "core/settings.h" #include "video_core/pica_state.h" #include "video_core/renderer_base.h" #include "video_core/renderer_opengl/gl_rasterizer_cache.h" @@ -93,9 +98,12 @@ static const FormatTuple& GetFormatTuple(PixelFormat pixel_format) { * Originally from https://github.com/apitrace/apitrace/blob/master/retrace/glstate_images.cpp */ static inline void GetTexImageOES(GLenum target, GLint level, GLenum format, GLenum type, - GLint height, GLint width, GLint depth, GLubyte* pixels) { + GLint height, GLint width, GLint depth, GLubyte* pixels, + GLuint size) { + memset(pixels, 0x80, size); - memset(pixels, 0x80, height * width * 4); + OpenGLState cur_state = OpenGLState::GetCurState(); + OpenGLState state; GLenum texture_binding = GL_NONE; switch (target) { @@ -122,11 +130,10 @@ static inline void GetTexImageOES(GLenum target, GLint level, GLenum format, GLe return; } - GLint prev_fbo = 0; - GLuint fbo = 0; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo); - glGenFramebuffers(1, &fbo); - glBindFramebuffer(GL_FRAMEBUFFER, fbo); + OGLFramebuffer fbo; + fbo.Create(); + state.draw.read_framebuffer = fbo.handle; + state.Apply(); switch (target) { case GL_TEXTURE_2D: @@ -136,8 +143,9 @@ static inline void GetTexImageOES(GLenum target, GLint level, GLenum format, GLe case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: case GL_TEXTURE_CUBE_MAP_POSITIVE_Z: case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: { - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level); - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, + level); + GLenum status = glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) { LOG_DEBUG(Render_OpenGL, "Framebuffer is incomplete, status: {:X}", status); } @@ -146,16 +154,16 @@ static inline void GetTexImageOES(GLenum target, GLint level, GLenum format, GLe } case GL_TEXTURE_3D_OES: for (int i = 0; i < depth; i++) { - glFramebufferTexture3D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, - level, i); + glFramebufferTexture3D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, + texture, level, i); glReadPixels(0, 0, width, height, format, type, pixels + 4 * i * width * height); } break; } - glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo); + cur_state.Apply(); - glDeleteFramebuffers(1, &fbo); + fbo.Release(); } template @@ -850,6 +858,90 @@ void CachedSurface::FlushGLBuffer(PAddr flush_start, PAddr flush_end) { } } +bool CachedSurface::LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_info, + Common::Rectangle& custom_rect) { + bool result = false; + auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache(); + const auto& image_interface = Core::System::GetInstance().GetImageInterface(); + + if (custom_tex_cache.IsTextureCached(tex_hash)) { + tex_info = custom_tex_cache.LookupTexture(tex_hash); + result = true; + } else { + if (custom_tex_cache.CustomTextureExists(tex_hash)) { + const auto& path_info = custom_tex_cache.LookupTexturePathInfo(tex_hash); + if (image_interface->DecodePNG(tex_info.tex, tex_info.width, tex_info.height, + path_info.path)) { + 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); + custom_tex_cache.CacheTexture(tex_hash, tex_info.tex, tex_info.width, + tex_info.height); + result = true; + } 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); + } + } + } + + if (result) { + custom_rect.left = (custom_rect.left / width) * tex_info.width; + custom_rect.top = (custom_rect.top / height) * tex_info.height; + custom_rect.right = (custom_rect.right / width) * tex_info.width; + custom_rect.bottom = (custom_rect.bottom / height) * tex_info.height; + } + + return result; +} + +void CachedSurface::DumpTexture(GLuint target_tex, u64 tex_hash) { + // Dump texture to RGBA8 and encode as PNG + const auto& image_interface = Core::System::GetInstance().GetImageInterface(); + auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache(); + std::string dump_path = + fmt::format("{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), + Core::System::GetInstance().Kernel().GetCurrentProcess()->codeset->program_id); + if (!FileUtil::CreateFullPath(dump_path)) { + LOG_ERROR(Render, "Unable to create {}", dump_path); + return; + } + + dump_path += fmt::format("tex1_{}x{}_{:016X}_{}.png", width, height, tex_hash, + static_cast(pixel_format)); + if (!custom_tex_cache.IsTextureDumped(tex_hash) && !FileUtil::Exists(dump_path)) { + custom_tex_cache.SetTextureDumped(tex_hash); + + LOG_INFO(Render_OpenGL, "Dumping texture to {}", dump_path); + std::vector decoded_texture; + decoded_texture.resize(width * height * 4); + glBindTexture(GL_TEXTURE_2D, target_tex); + /* + GetTexImageOES is used even if not using OpenGL ES to work around a small issue that + happens if using custom textures with texture dumping at the same. + Let's say there's 2 textures that are both 32x32 and one of them gets replaced with a + higher quality 256x256 texture. If the 256x256 texture is displayed first and the 32x32 + texture gets uploaded to the same underlying OpenGL texture, the 32x32 texture will + appear in the corner of the 256x256 texture. + If texture dumping is enabled and the 32x32 is undumped, Citra will attempt to dump it. + Since the underlying OpenGL texture is still 256x256, Citra crashes because it thinks the + texture is only 32x32. + GetTexImageOES conveniently only dumps the specified region, and works on both + desktop and ES. + */ + GetTexImageOES(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, height, width, 0, + &decoded_texture[0], decoded_texture.size()); + glBindTexture(GL_TEXTURE_2D, 0); + Common::FlipRGBA8Texture(decoded_texture, width, height); + if (!image_interface->EncodePNG(dump_path, decoded_texture, width, height)) + LOG_ERROR(Render_OpenGL, "Failed to save decoded texture"); + } +} + MICROPROFILE_DEFINE(OpenGL_TextureUL, "OpenGL", "Texture Upload", MP_RGB(128, 192, 64)); void CachedSurface::UploadGLTexture(const Common::Rectangle& rect, GLuint read_fb_handle, GLuint draw_fb_handle) { @@ -860,9 +952,22 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle& rect, GLuint r ASSERT(gl_buffer_size == width * height * GetGLBytesPerPixel(pixel_format)); + // Read custom texture + auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache(); + std::string dump_path; // Has to be declared here for logging later + u64 tex_hash = 0; + // Required for rect to function properly with custom textures + Common::Rectangle custom_rect = rect; + + if (Settings::values.dump_textures || Settings::values.custom_textures) + tex_hash = Common::ComputeHash64(gl_buffer.get(), gl_buffer_size); + + if (Settings::values.custom_textures) + is_custom = LoadCustomTexture(tex_hash, custom_tex_info, custom_rect); + // Load data from memory to the surface - GLint x0 = static_cast(rect.left); - GLint y0 = static_cast(rect.bottom); + GLint x0 = static_cast(custom_rect.left); + GLint y0 = static_cast(custom_rect.bottom); std::size_t buffer_offset = (y0 * stride + x0) * GetGLBytesPerPixel(pixel_format); const FormatTuple& tuple = GetFormatTuple(pixel_format); @@ -876,7 +981,13 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle& rect, GLuint r y0 = 0; unscaled_tex.Create(); - AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth(), rect.GetHeight()); + if (is_custom) { + AllocateSurfaceTexture(unscaled_tex.handle, GetFormatTuple(PixelFormat::RGBA8), + custom_tex_info.width, custom_tex_info.height); + } else { + AllocateSurfaceTexture(unscaled_tex.handle, tuple, custom_rect.GetWidth(), + custom_rect.GetHeight()); + } target_tex = unscaled_tex.handle; } @@ -888,27 +999,44 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle& rect, GLuint r // Ensure no bad interactions with GL_UNPACK_ALIGNMENT ASSERT(stride * GetGLBytesPerPixel(pixel_format) % 4 == 0); - glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(stride)); + if (is_custom) { + if (res_scale == 1) { + AllocateSurfaceTexture(texture.handle, GetFormatTuple(PixelFormat::RGBA8), + custom_tex_info.width, custom_tex_info.height); + cur_state.texture_units[0].texture_2d = texture.handle; + cur_state.Apply(); + } + // always going to be using rgba8 + glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(custom_tex_info.width)); - glActiveTexture(GL_TEXTURE0); - glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, static_cast(rect.GetWidth()), - static_cast(rect.GetHeight()), tuple.format, tuple.type, - &gl_buffer[buffer_offset]); + glActiveTexture(GL_TEXTURE0); + glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, custom_tex_info.width, custom_tex_info.height, + GL_RGBA, GL_UNSIGNED_BYTE, custom_tex_info.tex.data()); + } else { + glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(stride)); + + glActiveTexture(GL_TEXTURE0); + glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, static_cast(rect.GetWidth()), + static_cast(rect.GetHeight()), tuple.format, tuple.type, + &gl_buffer[buffer_offset]); + } glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + if (Settings::values.dump_textures && !is_custom) + DumpTexture(target_tex, tex_hash); cur_state.texture_units[0].texture_2d = old_tex; cur_state.Apply(); if (res_scale != 1) { - auto scaled_rect = rect; + auto scaled_rect = custom_rect; scaled_rect.left *= res_scale; scaled_rect.top *= res_scale; scaled_rect.right *= res_scale; scaled_rect.bottom *= res_scale; - BlitTextures(unscaled_tex.handle, {0, rect.GetHeight(), rect.GetWidth(), 0}, texture.handle, - scaled_rect, type, read_fb_handle, draw_fb_handle); + BlitTextures(unscaled_tex.handle, {0, custom_rect.GetHeight(), custom_rect.GetWidth(), 0}, + texture.handle, scaled_rect, type, read_fb_handle, draw_fb_handle); } InvalidateAllWatcher(); @@ -961,7 +1089,8 @@ void CachedSurface::DownloadGLTexture(const Common::Rectangle& rect, GLuint glActiveTexture(GL_TEXTURE0); if (GLES) { GetTexImageOES(GL_TEXTURE_2D, 0, tuple.format, tuple.type, rect.GetHeight(), - rect.GetWidth(), 0, &gl_buffer[buffer_offset]); + rect.GetWidth(), 0, &gl_buffer[buffer_offset], + gl_buffer_size - buffer_offset); } else { glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, &gl_buffer[buffer_offset]); } @@ -1411,12 +1540,23 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf state.Apply(); glActiveTexture(GL_TEXTURE0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, max_level); - u32 width = surface->width * surface->res_scale; - u32 height = surface->height * surface->res_scale; + u32 width; + u32 height; + if (surface->is_custom) { + width = surface->custom_tex_info.width; + height = surface->custom_tex_info.height; + } else { + width = surface->width * surface->res_scale; + height = surface->height * surface->res_scale; + } for (u32 level = surface->max_level + 1; level <= max_level; ++level) { glTexImage2D(GL_TEXTURE_2D, level, format_tuple.internal_format, width >> level, height >> level, 0, format_tuple.format, format_tuple.type, nullptr); } + if (surface->is_custom) { + // TODO: proper mipmap support for custom textures + glGenerateMipmap(GL_TEXTURE_2D); + } surface->max_level = max_level; } @@ -1449,21 +1589,23 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf } state.ResetTexture(level_surface->texture.handle); state.Apply(); - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - level_surface->texture.handle, 0); - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, - GL_TEXTURE_2D, 0, 0); + if (!surface->is_custom) { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + level_surface->texture.handle, 0); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_TEXTURE_2D, 0, 0); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - surface->texture.handle, level); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, - GL_TEXTURE_2D, 0, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + surface->texture.handle, level); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_TEXTURE_2D, 0, 0); - auto src_rect = level_surface->GetScaledRect(); - auto dst_rect = params.GetScaledRect(); - glBlitFramebuffer(src_rect.left, src_rect.bottom, src_rect.right, src_rect.top, - dst_rect.left, dst_rect.bottom, dst_rect.right, dst_rect.top, - GL_COLOR_BUFFER_BIT, GL_LINEAR); + auto src_rect = level_surface->GetScaledRect(); + auto dst_rect = params.GetScaledRect(); + glBlitFramebuffer(src_rect.left, src_rect.bottom, src_rect.right, src_rect.top, + dst_rect.left, dst_rect.bottom, dst_rect.right, dst_rect.top, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + } watcher->Validate(); } } diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.h b/src/video_core/renderer_opengl/gl_rasterizer_cache.h index 962cbceb6..cd601ef29 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.h +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.h @@ -25,6 +25,7 @@ #include "common/common_funcs.h" #include "common/common_types.h" #include "common/math_util.h" +#include "core/custom_tex_cache.h" #include "core/hw/gpu.h" #include "video_core/regs_framebuffer.h" #include "video_core/regs_texturing.h" @@ -361,6 +362,9 @@ struct CachedSurface : SurfaceParams, std::enable_shared_from_this, 7> level_watchers; + bool is_custom = false; + Core::CustomTexInfo custom_tex_info; + static constexpr unsigned int GetGLBytesPerPixel(PixelFormat format) { // OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type return format == PixelFormat::Invalid @@ -377,6 +381,11 @@ struct CachedSurface : SurfaceParams, std::enable_shared_from_this& custom_rect); + void DumpTexture(GLuint target_tex, u64 tex_hash); + // Upload/Download data in gl_buffer in/to this surface's texture void UploadGLTexture(const Common::Rectangle& rect, GLuint read_fb_handle, GLuint draw_fb_handle);