Merge pull request #4868 from khang06/tex-dump

Texture dumping and replacement
This commit is contained in:
James Rowe 2019-11-09 13:06:19 -07:00 committed by GitHub
commit 4f19d380f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1208 additions and 356 deletions

3
.gitmodules vendored
View file

@ -43,3 +43,6 @@
[submodule "teakra"] [submodule "teakra"]
path = externals/teakra path = externals/teakra
url = https://github.com/wwylele/teakra.git url = https://github.com/wwylele/teakra.git
[submodule "lodepng"]
path = externals/lodepng/lodepng
url = https://github.com/lvandeve/lodepng.git

View file

@ -100,3 +100,6 @@ if (ENABLE_WEB_SERVICE)
add_library(cpp-jwt INTERFACE) add_library(cpp-jwt INTERFACE)
target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include) target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include)
endif() endif()
# lodepng
add_subdirectory(lodepng)

7
externals/lodepng/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,7 @@
add_library(lodepng
lodepng/lodepng.cpp
lodepng/lodepng.h
)
create_target_directory_groups(lodepng)
target_include_directories(lodepng INTERFACE lodepng)

1
externals/lodepng/lodepng vendored Submodule

@ -0,0 +1 @@
Subproject commit 31d9704fdcca0b68fb9656d4764fa0fb60e460c2

View file

@ -8,13 +8,15 @@ add_executable(citra
default_ini.h default_ini.h
emu_window/emu_window_sdl2.cpp emu_window/emu_window_sdl2.cpp
emu_window/emu_window_sdl2.h emu_window/emu_window_sdl2.h
lodepng_image_interface.cpp
lodepng_image_interface.h
resource.h resource.h
) )
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) target_link_libraries(citra PRIVATE inih glad lodepng)
if (MSVC) if (MSVC)
target_link_libraries(citra PRIVATE getopt) target_link_libraries(citra PRIVATE getopt)
endif() endif()

View file

@ -20,6 +20,7 @@
#include "citra/config.h" #include "citra/config.h"
#include "citra/emu_window/emu_window_sdl2.h" #include "citra/emu_window/emu_window_sdl2.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"
@ -342,6 +343,9 @@ 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>());
std::unique_ptr<EmuWindow_SDL2> emu_window{std::make_unique<EmuWindow_SDL2>(fullscreen)}; std::unique_ptr<EmuWindow_SDL2> emu_window{std::make_unique<EmuWindow_SDL2>(fullscreen)};
Core::System& system{Core::System::GetInstance()}; Core::System& system{Core::System::GetInstance()};

View file

@ -162,6 +162,12 @@ void Config::ReadValues() {
Settings::values.custom_bottom_bottom = Settings::values.custom_bottom_bottom =
static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_bottom", 480)); static_cast<u16>(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 // Audio
Settings::values.enable_dsp_lle = sdl2_config->GetBoolean("Audio", "enable_dsp_lle", false); Settings::values.enable_dsp_lle = sdl2_config->GetBoolean("Audio", "enable_dsp_lle", false);
Settings::values.enable_dsp_lle_multithread = Settings::values.enable_dsp_lle_multithread =

View file

@ -178,6 +178,18 @@ custom_bottom_bottom =
# 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent # 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent
swap_screen = 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] [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

View file

@ -0,0 +1,29 @@
// 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;
}

View file

@ -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<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;
};

View file

@ -47,6 +47,8 @@ add_executable(citra-qt
configuration/configure_debug.cpp configuration/configure_debug.cpp
configuration/configure_debug.h configuration/configure_debug.h
configuration/configure_debug.ui configuration/configure_debug.ui
configuration/configure_enhancements.cpp
configuration/configure_enhancements.h
configuration/configure_dialog.cpp configuration/configure_dialog.cpp
configuration/configure_dialog.h configuration/configure_dialog.h
configuration/configure_general.cpp configuration/configure_general.cpp
@ -142,6 +144,8 @@ add_executable(citra-qt
multiplayer/validation.h multiplayer/validation.h
uisettings.cpp uisettings.cpp
uisettings.h uisettings.h
qt_image_interface.cpp
qt_image_interface.h
updater/updater.cpp updater/updater.cpp
updater/updater.h updater/updater.h
updater/updater_p.h updater/updater_p.h

View file

@ -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, 19> default_hotkeys{ const std::array<UISettings::Shortcut, 20> 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::ApplicationShortcut}}, {QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::ApplicationShortcut}},
{QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}}, {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}},
@ -75,7 +75,8 @@ const std::array<UISettings::Shortcut, 19> default_hotkeys{
{QStringLiteral("Toggle Frame Advancing"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+A"), Qt::ApplicationShortcut}}, {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 Screen Layout"), QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::WindowShortcut}},
{QStringLiteral("Toggle Speed Limit"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+Z"), Qt::ApplicationShortcut}}, {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 // clang-format on
void Config::ReadValues() { void Config::ReadValues() {
@ -230,6 +231,16 @@ void Config::ReadControlValues() {
qt_config->endGroup(); 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() { void Config::ReadCoreValues() {
qt_config->beginGroup(QStringLiteral("Core")); qt_config->beginGroup(QStringLiteral("Core"));
@ -693,6 +704,16 @@ void Config::SaveControlValues() {
qt_config->endGroup(); 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() { void Config::SaveCoreValues() {
qt_config->beginGroup(QStringLiteral("Core")); qt_config->beginGroup(QStringLiteral("Core"));

View file

@ -42,6 +42,7 @@ private:
void ReadUIGameListValues(); void ReadUIGameListValues();
void ReadUILayoutValues(); void ReadUILayoutValues();
void ReadUpdaterValues(); void ReadUpdaterValues();
void ReadUtilityValues();
void ReadWebServiceValues(); void ReadWebServiceValues();
void SaveValues(); void SaveValues();
@ -62,6 +63,7 @@ private:
void SaveUIGameListValues(); void SaveUIGameListValues();
void SaveUILayoutValues(); void SaveUILayoutValues();
void SaveUpdaterValues(); void SaveUpdaterValues();
void SaveUtilityValues();
void SaveWebServiceValues(); void SaveWebServiceValues();
QVariant ReadSetting(const QString& name) const; QVariant ReadSetting(const QString& name) const;

View file

@ -48,6 +48,11 @@
<string>Graphics</string> <string>Graphics</string>
</attribute> </attribute>
</widget> </widget>
<widget class="ConfigureEnhancements" name="enhancementsTab">
<attribute name="title">
<string>Enhancements</string>
</attribute>
</widget>
<widget class="ConfigureAudio" name="audioTab"> <widget class="ConfigureAudio" name="audioTab">
<attribute name="title"> <attribute name="title">
<string>Audio</string> <string>Audio</string>
@ -135,6 +140,12 @@
<header>configuration/configure_graphics.h</header> <header>configuration/configure_graphics.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>ConfigureEnhancements</class>
<extends>QWidget</extends>
<header>configuration/configure_enhancements.h</header>
<container>1</container>
</customwidget>
<customwidget> <customwidget>
<class>ConfigureWeb</class> <class>ConfigureWeb</class>
<extends>QWidget</extends> <extends>QWidget</extends>

View file

@ -45,6 +45,7 @@ void ConfigureDialog::SetConfiguration() {
ui->systemTab->SetConfiguration(); ui->systemTab->SetConfiguration();
ui->inputTab->LoadConfiguration(); ui->inputTab->LoadConfiguration();
ui->graphicsTab->SetConfiguration(); ui->graphicsTab->SetConfiguration();
ui->enhancementsTab->SetConfiguration();
ui->audioTab->SetConfiguration(); ui->audioTab->SetConfiguration();
ui->cameraTab->SetConfiguration(); ui->cameraTab->SetConfiguration();
ui->debugTab->SetConfiguration(); ui->debugTab->SetConfiguration();
@ -59,6 +60,7 @@ void ConfigureDialog::ApplyConfiguration() {
ui->inputTab->ApplyProfile(); ui->inputTab->ApplyProfile();
ui->hotkeysTab->ApplyConfiguration(registry); ui->hotkeysTab->ApplyConfiguration(registry);
ui->graphicsTab->ApplyConfiguration(); ui->graphicsTab->ApplyConfiguration();
ui->enhancementsTab->ApplyConfiguration();
ui->audioTab->ApplyConfiguration(); ui->audioTab->ApplyConfiguration();
ui->cameraTab->ApplyConfiguration(); ui->cameraTab->ApplyConfiguration();
ui->debugTab->ApplyConfiguration(); ui->debugTab->ApplyConfiguration();
@ -76,7 +78,7 @@ void ConfigureDialog::PopulateSelectionList() {
const std::array<std::pair<QString, QList<QWidget*>>, 4> items{ const std::array<std::pair<QString, QList<QWidget*>>, 4> items{
{{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->uiTab}}, {{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->uiTab}},
{tr("System"), {ui->systemTab, ui->audioTab, ui->cameraTab}}, {tr("System"), {ui->systemTab, ui->audioTab, ui->cameraTab}},
{tr("Graphics"), {ui->graphicsTab}}, {tr("Graphics"), {ui->enhancementsTab, ui->graphicsTab}},
{tr("Controls"), {ui->inputTab, ui->hotkeysTab}}}}; {tr("Controls"), {ui->inputTab, ui->hotkeysTab}}}};
for (const auto& entry : items) { for (const auto& entry : items) {
@ -109,6 +111,7 @@ void ConfigureDialog::RetranslateUI() {
ui->inputTab->RetranslateUI(); ui->inputTab->RetranslateUI();
ui->hotkeysTab->RetranslateUI(); ui->hotkeysTab->RetranslateUI();
ui->graphicsTab->RetranslateUI(); ui->graphicsTab->RetranslateUI();
ui->enhancementsTab->RetranslateUI();
ui->audioTab->RetranslateUI(); ui->audioTab->RetranslateUI();
ui->cameraTab->RetranslateUI(); ui->cameraTab->RetranslateUI();
ui->debugTab->RetranslateUI(); ui->debugTab->RetranslateUI();
@ -121,12 +124,17 @@ void ConfigureDialog::UpdateVisibleTabs() {
if (items.isEmpty()) if (items.isEmpty())
return; return;
const std::map<QWidget*, QString> widgets = { const std::map<QWidget*, QString> widgets = {{ui->generalTab, tr("General")},
{ui->generalTab, tr("General")}, {ui->systemTab, tr("System")}, {ui->systemTab, tr("System")},
{ui->inputTab, tr("Input")}, {ui->hotkeysTab, tr("Hotkeys")}, {ui->inputTab, tr("Input")},
{ui->graphicsTab, tr("Graphics")}, {ui->audioTab, tr("Audio")}, {ui->hotkeysTab, tr("Hotkeys")},
{ui->cameraTab, tr("Camera")}, {ui->debugTab, tr("Debug")}, {ui->enhancementsTab, tr("Enhancements")},
{ui->webTab, tr("Web")}, {ui->uiTab, tr("UI")}}; {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(); ui->tabWidget->clear();

View file

@ -0,0 +1,110 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <QColorDialog>
#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<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
[this](int currentIndex) {
updateShaders(static_cast<Settings::StereoRenderOption>(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<int>(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<int>(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<u16>(ui->resolution_factor_combobox->currentIndex());
Settings::values.render_3d =
static_cast<Settings::StereoRenderOption>(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<Settings::LayoutOption>(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<float>(bg_color.redF());
Settings::values.bg_green = static_cast<float>(bg_color.greenF());
Settings::values.bg_blue = static_cast<float>(bg_color.blueF());
}
ConfigureEnhancements::~ConfigureEnhancements() {
delete ui;
}

View file

@ -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 <QWidget>
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;
};

View file

@ -0,0 +1,317 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConfigureEnhancements</class>
<widget class="QWidget" name="ConfigureEnhancements">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>595</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="rendererBox">
<property name="title">
<string>Renderer</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Internal Resolution</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="resolution_factor_combobox">
<item>
<property name="text">
<string>Auto (Window Size)</string>
</property>
</item>
<item>
<property name="text">
<string>Native (400x240)</string>
</property>
</item>
<item>
<property name="text">
<string>2x Native (800x480)</string>
</property>
</item>
<item>
<property name="text">
<string>3x Native (1200x720)</string>
</property>
</item>
<item>
<property name="text">
<string>4x Native (1600x960)</string>
</property>
</item>
<item>
<property name="text">
<string>5x Native (2000x1200)</string>
</property>
</item>
<item>
<property name="text">
<string>6x Native (2400x1440)</string>
</property>
</item>
<item>
<property name="text">
<string>7x Native (2800x1680)</string>
</property>
</item>
<item>
<property name="text">
<string>8x Native (3200x1920)</string>
</property>
</item>
<item>
<property name="text">
<string>9x Native (3600x2160)</string>
</property>
</item>
<item>
<property name="text">
<string>10x Native (4000x2400)</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="toggle_linear_filter">
<property name="text">
<string>Enable Linear Filtering</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Post-Processing Shader</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="shader_combobox"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Stereoscopy</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Stereoscopic 3D Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="render_3d_combobox">
<item>
<property name="text">
<string>Off</string>
</property>
</item>
<item>
<property name="text">
<string>Side by Side</string>
</property>
</item>
<item>
<property name="text">
<string>Anaglyph</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Depth</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="factor_3d">
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="layoutBox">
<property name="title">
<string>Layout</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label1">
<property name="text">
<string>Screen Layout:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="layout_combobox">
<item>
<property name="text">
<string>Default</string>
</property>
</item>
<item>
<property name="text">
<string>Single Screen</string>
</property>
</item>
<item>
<property name="text">
<string>Large Screen</string>
</property>
</item>
<item>
<property name="text">
<string>Side by Side</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="swap_screen">
<property name="text">
<string>Swap Screens</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="bg_label">
<property name="text">
<string>Background Color:</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bg_button">
<property name="maximumSize">
<size>
<width>40</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="utilityBox">
<property name="title">
<string>Utility</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QCheckBox" name="toggle_custom_textures">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Replace textures with PNG files.&lt;/p&gt;&lt;p&gt;Textures are loaded from load/textures/[Title ID]/.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Use Custom Textures</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="toggle_dump_textures">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Dump textures to PNG files.&lt;/p&gt;&lt;p&gt;Textures are dumped to dump/textures/[Title ID]/.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Dump Textures</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="toggle_preload_textures">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Load all custom textures into memory on boot, instead of loading them when the game requires them.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Preload Custom Textures</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>165</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -25,6 +25,10 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)
ConfigureGeneral::~ConfigureGeneral() = default; ConfigureGeneral::~ConfigureGeneral() = default;
void ConfigureGeneral::SetConfiguration() { 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_check_exit->setChecked(UISettings::values.confirm_before_closing);
ui->toggle_background_pause->setChecked(UISettings::values.pause_when_in_background); ui->toggle_background_pause->setChecked(UISettings::values.pause_when_in_background);
@ -53,6 +57,9 @@ void ConfigureGeneral::ResetDefaults() {
} }
void ConfigureGeneral::ApplyConfiguration() { 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.confirm_before_closing = ui->toggle_check_exit->isChecked();
UISettings::values.pause_when_in_background = ui->toggle_background_pause->isChecked(); UISettings::values.pause_when_in_background = ui->toggle_background_pause->isChecked();

View file

@ -17,11 +17,12 @@ ConfigureGraphics::ConfigureGraphics(QWidget* parent)
ui->setupUi(this); ui->setupUi(this);
SetConfiguration(); SetConfiguration();
ui->layoutBox->setEnabled(!Settings::values.custom_layout);
ui->hw_renderer_group->setEnabled(ui->toggle_hw_renderer->isChecked()); ui->hw_renderer_group->setEnabled(ui->toggle_hw_renderer->isChecked());
connect(ui->toggle_hw_renderer, &QCheckBox::toggled, ui->hw_renderer_group, connect(ui->toggle_hw_renderer, &QCheckBox::toggled, this, [this] {
&QWidget::setEnabled); auto checked = ui->toggle_hw_renderer->isChecked();
ui->hw_renderer_group->setEnabled(checked);
});
ui->hw_shader_group->setEnabled(ui->toggle_hw_shader->isChecked()); ui->hw_shader_group->setEnabled(ui->toggle_hw_shader->isChecked());
connect(ui->toggle_hw_shader, &QCheckBox::toggled, ui->hw_shader_group, &QWidget::setEnabled); connect(ui->toggle_hw_shader, &QCheckBox::toggled, ui->hw_shader_group, &QWidget::setEnabled);
#ifdef __APPLE__ #ifdef __APPLE__
@ -36,21 +37,6 @@ ConfigureGraphics::ConfigureGraphics(QWidget* parent)
} }
}); });
#endif #endif
connect(ui->render_3d_combobox,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
[this](int currentIndex) {
updateShaders(static_cast<Settings::StereoRenderOption>(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; ConfigureGraphics::~ConfigureGraphics() = default;
@ -60,15 +46,6 @@ void ConfigureGraphics::SetConfiguration() {
ui->toggle_hw_shader->setChecked(Settings::values.use_hw_shader); ui->toggle_hw_shader->setChecked(Settings::values.use_hw_shader);
ui->toggle_accurate_mul->setChecked(Settings::values.shaders_accurate_mul); ui->toggle_accurate_mul->setChecked(Settings::values.shaders_accurate_mul);
ui->toggle_shader_jit->setChecked(Settings::values.use_shader_jit); 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<int>(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<int>(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() { void ConfigureGraphics::ApplyConfiguration() {
@ -76,47 +53,6 @@ void ConfigureGraphics::ApplyConfiguration() {
Settings::values.use_hw_shader = ui->toggle_hw_shader->isChecked(); Settings::values.use_hw_shader = ui->toggle_hw_shader->isChecked();
Settings::values.shaders_accurate_mul = ui->toggle_accurate_mul->isChecked(); Settings::values.shaders_accurate_mul = ui->toggle_accurate_mul->isChecked();
Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked(); Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked();
Settings::values.resolution_factor =
static_cast<u16>(ui->resolution_factor_combobox->currentIndex());
Settings::values.render_3d =
static_cast<Settings::StereoRenderOption>(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<Settings::LayoutOption>(ui->layout_combobox->currentIndex());
Settings::values.swap_screen = ui->swap_screen->isChecked();
Settings::values.bg_red = static_cast<float>(bg_color.redF());
Settings::values.bg_green = static_cast<float>(bg_color.greenF());
Settings::values.bg_blue = static_cast<float>(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() { void ConfigureGraphics::RetranslateUI() {

View file

@ -26,7 +26,4 @@ public:
std::unique_ptr<Ui::ConfigureGraphics> ui; std::unique_ptr<Ui::ConfigureGraphics> ui;
QColor bg_color; QColor bg_color;
private:
void updateShaders(bool anaglyph);
}; };

View file

@ -7,9 +7,15 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>400</width> <width>400</width>
<height>603</height> <height>430</height>
</rect> </rect>
</property> </property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string>Form</string>
</property> </property>
@ -45,76 +51,6 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Internal Resolution</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="resolution_factor_combobox">
<item>
<property name="text">
<string>Auto (Window Size)</string>
</property>
</item>
<item>
<property name="text">
<string>Native (400x240)</string>
</property>
</item>
<item>
<property name="text">
<string>2x Native (800x480)</string>
</property>
</item>
<item>
<property name="text">
<string>3x Native (1200x720)</string>
</property>
</item>
<item>
<property name="text">
<string>4x Native (1600x960)</string>
</property>
</item>
<item>
<property name="text">
<string>5x Native (2000x1200)</string>
</property>
</item>
<item>
<property name="text">
<string>6x Native (2400x1440)</string>
</property>
</item>
<item>
<property name="text">
<string>7x Native (2800x1680)</string>
</property>
</item>
<item>
<property name="text">
<string>8x Native (3200x1920)</string>
</property>
</item>
<item>
<property name="text">
<string>9x Native (3600x2160)</string>
</property>
</item>
<item>
<property name="text">
<string>10x Native (4000x2400)</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item> <item>
<widget class="QCheckBox" name="toggle_hw_shader"> <widget class="QCheckBox" name="toggle_hw_shader">
<property name="toolTip"> <property name="toolTip">
@ -166,165 +102,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="toggle_linear_filter">
<property name="text">
<string>Enable Linear Filtering</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Post-Processing Shader</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="shader_combobox"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Stereoscopy</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Stereoscopic 3D Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="render_3d_combobox">
<item>
<property name="text">
<string>Off</string>
</property>
</item>
<item>
<property name="text">
<string>Side by Side</string>
</property>
</item>
<item>
<property name="text">
<string>Anaglyph</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Depth</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="factor_3d">
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="layoutBox">
<property name="title">
<string>Layout</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label1">
<property name="text">
<string>Screen Layout:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="layout_combobox">
<item>
<property name="text">
<string>Default</string>
</property>
</item>
<item>
<property name="text">
<string>Single Screen</string>
</property>
</item>
<item>
<property name="text">
<string>Large Screen</string>
</property>
</item>
<item>
<property name="text">
<string>Side by Side</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="swap_screen">
<property name="text">
<string>Swap Screens</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="bg_label">
<property name="text">
<string>Background Color:</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bg_button">
<property name="maximumSize">
<size>
<width>40</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -336,7 +113,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>20</width>
<height>40</height> <height>270</height>
</size> </size>
</property> </property>
</spacer> </spacer>

View file

@ -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_extdata_location = context_menu.addAction(tr("Open Extra Data Location"));
QAction* open_application_location = context_menu.addAction(tr("Open Application 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_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")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
const bool is_application = const bool is_application =
@ -484,6 +487,10 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra
program_id + 0xe00000000) + program_id + 0xe00000000) +
"content/")); "content/"));
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); 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()); navigate_to_gamedb_entry->setVisible(it != compatibility_list.end());
connect(open_save_location, &QAction::triggered, [this, program_id] { 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] { connect(open_update_location, &QAction::triggered, [this, program_id] {
emit OpenFolderRequested(program_id, GameListOpenTarget::UPDATE_DATA); 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]() { connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
emit NavigateToGamedbEntryRequested(program_id, compatibility_list); emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
}); });

View file

@ -29,7 +29,14 @@ class QTreeView;
class QToolButton; class QToolButton;
class QVBoxLayout; 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 { class GameList : public QWidget {
Q_OBJECT Q_OBJECT

View file

@ -50,6 +50,7 @@
#include "citra_qt/hotkeys.h" #include "citra_qt/hotkeys.h"
#include "citra_qt/main.h" #include "citra_qt/main.h"
#include "citra_qt/multiplayer/state.h" #include "citra_qt/multiplayer/state.h"
#include "citra_qt/qt_image_interface.h"
#include "citra_qt/uisettings.h" #include "citra_qt/uisettings.h"
#include "citra_qt/updater/updater.h" #include "citra_qt/updater/updater.h"
#include "citra_qt/util/clickable_label.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; Settings::values.use_frame_limit = !Settings::values.use_frame_limit;
UpdateStatusBar(); UpdateStatusBar();
}); });
// We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the connect(hotkey_registry.GetHotkey("Main Window", "Toggle Texture Dumping", this),
// variable hold a garbage value after this function exits &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; static constexpr u16 SPEED_LIMIT_STEP = 5;
connect(hotkey_registry.GetHotkey("Main Window", "Increase Speed Limit", this), connect(hotkey_registry.GetHotkey("Main Window", "Increase Speed Limit", this),
&QShortcut::activated, 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) + path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) +
"content/"; "content/";
break; 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: default:
LOG_ERROR(Frontend, "Unexpected target {}", static_cast<int>(target)); LOG_ERROR(Frontend, "Unexpected target {}", static_cast<int>(target));
return; return;
@ -2059,6 +2073,9 @@ int main(int argc, char* argv[]) {
Core::System::GetInstance().RegisterMiiSelector(std::make_shared<QtMiiSelector>(main_window)); Core::System::GetInstance().RegisterMiiSelector(std::make_shared<QtMiiSelector>(main_window));
Core::System::GetInstance().RegisterSoftwareKeyboard(std::make_shared<QtKeyboard>(main_window)); Core::System::GetInstance().RegisterSoftwareKeyboard(std::make_shared<QtKeyboard>(main_window));
// Register Qt image interface
Core::System::GetInstance().RegisterImageInterface(std::make_shared<QtImageInterface>());
main_window.show(); main_window.show();
QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window,

View file

@ -0,0 +1,38 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <QImage>
#include <QString>
#include "citra_qt/qt_image_interface.h"
#include "common/logging/log.h"
bool QtImageInterface::DecodePNG(std::vector<u8>& 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<u8>(image.constBits(), image.constBits() + (width * height * 4));
return true;
}
bool QtImageInterface::EncodePNG(const std::string& path, const std::vector<u8>& 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;
}

View file

@ -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<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;
};

View file

@ -84,6 +84,8 @@ add_library(common STATIC
swap.h swap.h
telemetry.cpp telemetry.cpp
telemetry.h telemetry.h
texture.cpp
texture.h
thread.cpp thread.cpp
thread.h thread.h
thread_queue_list.h thread_queue_list.h

View file

@ -44,6 +44,9 @@
#define CHEATS_DIR "cheats" #define CHEATS_DIR "cheats"
#define DLL_DIR "external_dlls" #define DLL_DIR "external_dlls"
#define SHADER_DIR "shaders" #define SHADER_DIR "shaders"
#define DUMP_DIR "dump"
#define LOAD_DIR "load"
#define SHADER_DIR "shaders"
// Filenames // Filenames
// Files in the directory returned by GetUserPath(UserPath::LogDir) // Files in the directory returned by GetUserPath(UserPath::LogDir)

View file

@ -469,6 +469,17 @@ u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry,
return ForeachDirectoryEntry(&num_entries, directory, callback) ? num_entries : 0; return ForeachDirectoryEntry(&num_entries, directory, callback) ? num_entries : 0;
} }
void GetAllFilesFromNestedEntries(FSTEntry& directory, std::vector<FSTEntry>& output) {
std::vector<FSTEntry> 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) { bool DeleteDirRecursively(const std::string& directory, unsigned int recursion) {
const auto callback = [recursion](u64* num_entries_out, const std::string& directory, const auto callback = [recursion](u64* num_entries_out, const std::string& directory,
const std::string& virtual_name) -> bool { 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::CheatsDir, user_path + CHEATS_DIR DIR_SEP);
g_paths.emplace(UserPath::DLLDir, user_path + DLL_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::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) { const std::string& GetUserPath(UserPath path) {

View file

@ -26,6 +26,8 @@ enum class UserPath {
CheatsDir, CheatsDir,
ConfigDir, ConfigDir,
DLLDir, DLLDir,
DumpDir,
LoadDir,
LogDir, LogDir,
NANDDir, NANDDir,
RootDir, RootDir,
@ -113,6 +115,13 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory,
u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry, u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry,
unsigned int recursion = 0); 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<FSTEntry>& output);
// deletes the given directory and anything under it. Returns true on success. // deletes the given directory and anything under it. Returns true on success.
bool DeleteDirRecursively(const std::string& directory, unsigned int recursion = 256); bool DeleteDirRecursively(const std::string& directory, unsigned int recursion = 256);

22
src/common/texture.cpp Normal file
View file

@ -0,0 +1,22 @@
// Copyright 2019 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <vector>
#include "common/assert.h"
#include "common/common_types.h"
namespace Common {
void FlipRGBA8Texture(std::vector<u8>& 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

12
src/common/texture.h Normal file
View file

@ -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 <vector>
#include "common/common_types.h"
namespace Common {
void FlipRGBA8Texture(std::vector<u8>& tex, u64 width, u64 height);
}

View file

@ -36,6 +36,8 @@ 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
@ -100,6 +102,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.h
frontend/input.h frontend/input.h
frontend/mic.h frontend/mic.h
frontend/mic.cpp frontend/mic.cpp

View file

@ -8,6 +8,7 @@
#include "audio_core/hle/hle.h" #include "audio_core/hle/hle.h"
#include "audio_core/lle/lle.h" #include "audio_core/lle/lle.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/texture.h"
#include "core/arm/arm_interface.h" #include "core/arm/arm_interface.h"
#ifdef ARCHITECTURE_x86_64 #ifdef ARCHITECTURE_x86_64
#include "core/arm/dynarmic/arm_dynarmic.h" #include "core/arm/dynarmic/arm_dynarmic.h"
@ -20,6 +21,7 @@
#ifdef ENABLE_FFMPEG_VIDEO_DUMPER #ifdef ENABLE_FFMPEG_VIDEO_DUMPER
#include "core/dumping/ffmpeg_backend.h" #include "core/dumping/ffmpeg_backend.h"
#endif #endif
#include "core/custom_tex_cache.h"
#include "core/gdbstub/gdbstub.h" #include "core/gdbstub/gdbstub.h"
#include "core/hle/kernel/client_port.h" #include "core/hle/kernel/client_port.h"
#include "core/hle/kernel/kernel.h" #include "core/hle/kernel/kernel.h"
@ -152,6 +154,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) {
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; status = ResultStatus::Success;
m_emu_window = &emu_window; m_emu_window = &emu_window;
m_filepath = filepath; m_filepath = filepath;
@ -298,6 +309,14 @@ const VideoDumper::Backend& System::VideoDumper() const {
return *video_dumper; 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<Frontend::MiiSelector> mii_selector) { void System::RegisterMiiSelector(std::shared_ptr<Frontend::MiiSelector> mii_selector) {
registered_mii_selector = std::move(mii_selector); registered_mii_selector = std::move(mii_selector);
} }
@ -306,6 +325,10 @@ void System::RegisterSoftwareKeyboard(std::shared_ptr<Frontend::SoftwareKeyboard
registered_swkbd = std::move(swkbd); registered_swkbd = std::move(swkbd);
} }
void System::RegisterImageInterface(std::shared_ptr<Frontend::ImageInterface> image_interface) {
registered_image_interface = std::move(image_interface);
}
void System::Shutdown() { void System::Shutdown() {
// Log last frame performance stats // Log last frame performance stats
const auto perf_results = GetAndResetPerfStats(); const auto perf_results = GetAndResetPerfStats();

View file

@ -7,8 +7,10 @@
#include <memory> #include <memory>
#include <string> #include <string>
#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"
@ -210,6 +212,15 @@ public:
/// Gets a const reference to the cheat engine /// Gets a const reference to the cheat engine
const Cheats::CheatEngine& CheatEngine() const; 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 /// Gets a reference to the video dumper backend
VideoDumper::Backend& VideoDumper(); VideoDumper::Backend& VideoDumper();
@ -248,6 +259,14 @@ public:
return registered_swkbd; return registered_swkbd;
} }
/// Image interface
void RegisterImageInterface(std::shared_ptr<Frontend::ImageInterface> image_interface);
std::shared_ptr<Frontend::ImageInterface> GetImageInterface() const {
return registered_image_interface;
}
private: private:
/** /**
* Initialize the emulated system. * Initialize the emulated system.
@ -289,6 +308,12 @@ private:
/// Video dumper backend /// Video dumper backend
std::unique_ptr<VideoDumper::Backend> video_dumper; std::unique_ptr<VideoDumper::Backend> video_dumper;
/// Custom texture cache system
std::unique_ptr<Core::CustomTexCache> custom_tex_cache;
/// Image interface
std::shared_ptr<Frontend::ImageInterface> registered_image_interface;
/// RPC Server for scripting support /// RPC Server for scripting support
std::unique_ptr<RPC::RPCServer> rpc_server; std::unique_ptr<RPC::RPCServer> rpc_server;

View file

@ -0,0 +1,111 @@
// 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() {
// 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<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() {
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

View file

@ -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 <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "common/common_types.h"
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();
void PreloadTextures();
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

View file

@ -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 <string>
#include <vector>
#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<u8>& dst, u32& width, u32& height,
const std::string& path) = 0;
virtual bool EncodePNG(const std::string& path, const std::vector<u8>& src, u32 width,
u32 height) = 0;
};
} // namespace Frontend

View file

@ -87,6 +87,8 @@ void LogSettings() {
LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d); LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d);
LogSetting("Layout_LayoutOption", static_cast<int>(Settings::values.layout_option)); LogSetting("Layout_LayoutOption", static_cast<int>(Settings::values.layout_option));
LogSetting("Layout_SwapScreen", Settings::values.swap_screen); 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_EnableDspLle", Settings::values.enable_dsp_lle);
LogSetting("Audio_EnableDspLleMultithread", Settings::values.enable_dsp_lle_multithread); LogSetting("Audio_EnableDspLleMultithread", Settings::values.enable_dsp_lle_multithread);
LogSetting("Audio_OutputEngine", Settings::values.sink_id); LogSetting("Audio_OutputEngine", Settings::values.sink_id);

View file

@ -170,6 +170,10 @@ struct Values {
bool filter_mode; bool filter_mode;
std::string pp_shader_name; std::string pp_shader_name;
bool dump_textures;
bool custom_textures;
bool preload_textures;
// Audio // Audio
bool enable_dsp_lle; bool enable_dsp_lle;
bool enable_dsp_lle_multithread; bool enable_dsp_lle_multithread;

View file

@ -21,9 +21,14 @@
#include "common/math_util.h" #include "common/math_util.h"
#include "common/microprofile.h" #include "common/microprofile.h"
#include "common/scope_exit.h" #include "common/scope_exit.h"
#include "common/texture.h"
#include "common/vector_math.h" #include "common/vector_math.h"
#include "core/core.h"
#include "core/custom_tex_cache.h"
#include "core/frontend/emu_window.h" #include "core/frontend/emu_window.h"
#include "core/hle/kernel/process.h"
#include "core/memory.h" #include "core/memory.h"
#include "core/settings.h"
#include "video_core/pica_state.h" #include "video_core/pica_state.h"
#include "video_core/renderer_base.h" #include "video_core/renderer_base.h"
#include "video_core/renderer_opengl/gl_rasterizer_cache.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 * 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, 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; GLenum texture_binding = GL_NONE;
switch (target) { switch (target) {
@ -122,11 +130,10 @@ static inline void GetTexImageOES(GLenum target, GLint level, GLenum format, GLe
return; return;
} }
GLint prev_fbo = 0; OGLFramebuffer fbo;
GLuint fbo = 0; fbo.Create();
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo); state.draw.read_framebuffer = fbo.handle;
glGenFramebuffers(1, &fbo); state.Apply();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
switch (target) { switch (target) {
case GL_TEXTURE_2D: 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_NEGATIVE_Y:
case GL_TEXTURE_CUBE_MAP_POSITIVE_Z: case GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: { case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture,
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); level);
GLenum status = glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) { if (status != GL_FRAMEBUFFER_COMPLETE) {
LOG_DEBUG(Render_OpenGL, "Framebuffer is incomplete, status: {:X}", status); 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: case GL_TEXTURE_3D_OES:
for (int i = 0; i < depth; i++) { for (int i = 0; i < depth; i++) {
glFramebufferTexture3D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, glFramebufferTexture3D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D,
level, i); texture, level, i);
glReadPixels(0, 0, width, height, format, type, pixels + 4 * i * width * height); glReadPixels(0, 0, width, height, format, type, pixels + 4 * i * width * height);
} }
break; break;
} }
glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo); cur_state.Apply();
glDeleteFramebuffers(1, &fbo); fbo.Release();
} }
template <typename Map, typename Interval> template <typename Map, typename Interval>
@ -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<u32>& 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<u32>(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<u8> 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)); MICROPROFILE_DEFINE(OpenGL_TextureUL, "OpenGL", "Texture Upload", MP_RGB(128, 192, 64));
void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint read_fb_handle, void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint read_fb_handle,
GLuint draw_fb_handle) { GLuint draw_fb_handle) {
@ -860,9 +952,22 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
ASSERT(gl_buffer_size == width * height * GetGLBytesPerPixel(pixel_format)); 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 // Load data from memory to the surface
GLint x0 = static_cast<GLint>(rect.left); GLint x0 = static_cast<GLint>(custom_rect.left);
GLint y0 = static_cast<GLint>(rect.bottom); GLint y0 = static_cast<GLint>(custom_rect.bottom);
std::size_t buffer_offset = (y0 * stride + x0) * GetGLBytesPerPixel(pixel_format); std::size_t buffer_offset = (y0 * stride + x0) * GetGLBytesPerPixel(pixel_format);
const FormatTuple& tuple = GetFormatTuple(pixel_format); const FormatTuple& tuple = GetFormatTuple(pixel_format);
@ -876,7 +981,13 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
y0 = 0; y0 = 0;
unscaled_tex.Create(); 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; target_tex = unscaled_tex.handle;
} }
@ -888,27 +999,44 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
// Ensure no bad interactions with GL_UNPACK_ALIGNMENT // Ensure no bad interactions with GL_UNPACK_ALIGNMENT
ASSERT(stride * GetGLBytesPerPixel(pixel_format) % 4 == 0); ASSERT(stride * GetGLBytesPerPixel(pixel_format) % 4 == 0);
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<GLint>(custom_tex_info.width));
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<GLint>(stride)); glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride));
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, static_cast<GLsizei>(rect.GetWidth()), glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, static_cast<GLsizei>(rect.GetWidth()),
static_cast<GLsizei>(rect.GetHeight()), tuple.format, tuple.type, static_cast<GLsizei>(rect.GetHeight()), tuple.format, tuple.type,
&gl_buffer[buffer_offset]); &gl_buffer[buffer_offset]);
}
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); 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.texture_units[0].texture_2d = old_tex;
cur_state.Apply(); cur_state.Apply();
if (res_scale != 1) { if (res_scale != 1) {
auto scaled_rect = rect; auto scaled_rect = custom_rect;
scaled_rect.left *= res_scale; scaled_rect.left *= res_scale;
scaled_rect.top *= res_scale; scaled_rect.top *= res_scale;
scaled_rect.right *= res_scale; scaled_rect.right *= res_scale;
scaled_rect.bottom *= res_scale; scaled_rect.bottom *= res_scale;
BlitTextures(unscaled_tex.handle, {0, rect.GetHeight(), rect.GetWidth(), 0}, texture.handle, BlitTextures(unscaled_tex.handle, {0, custom_rect.GetHeight(), custom_rect.GetWidth(), 0},
scaled_rect, type, read_fb_handle, draw_fb_handle); texture.handle, scaled_rect, type, read_fb_handle, draw_fb_handle);
} }
InvalidateAllWatcher(); InvalidateAllWatcher();
@ -961,7 +1089,8 @@ void CachedSurface::DownloadGLTexture(const Common::Rectangle<u32>& rect, GLuint
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
if (GLES) { if (GLES) {
GetTexImageOES(GL_TEXTURE_2D, 0, tuple.format, tuple.type, rect.GetHeight(), 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 { } else {
glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, &gl_buffer[buffer_offset]); 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(); state.Apply();
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, max_level); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, max_level);
u32 width = surface->width * surface->res_scale; u32 width;
u32 height = surface->height * surface->res_scale; 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) { for (u32 level = surface->max_level + 1; level <= max_level; ++level) {
glTexImage2D(GL_TEXTURE_2D, level, format_tuple.internal_format, width >> level, glTexImage2D(GL_TEXTURE_2D, level, format_tuple.internal_format, width >> level,
height >> level, 0, format_tuple.format, format_tuple.type, nullptr); 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; surface->max_level = max_level;
} }
@ -1449,6 +1589,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf
} }
state.ResetTexture(level_surface->texture.handle); state.ResetTexture(level_surface->texture.handle);
state.Apply(); state.Apply();
if (!surface->is_custom) {
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
level_surface->texture.handle, 0); level_surface->texture.handle, 0);
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
@ -1464,6 +1605,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf
glBlitFramebuffer(src_rect.left, src_rect.bottom, src_rect.right, src_rect.top, 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, dst_rect.left, dst_rect.bottom, dst_rect.right, dst_rect.top,
GL_COLOR_BUFFER_BIT, GL_LINEAR); GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
watcher->Validate(); watcher->Validate();
} }
} }

View file

@ -25,6 +25,7 @@
#include "common/common_funcs.h" #include "common/common_funcs.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "common/math_util.h" #include "common/math_util.h"
#include "core/custom_tex_cache.h"
#include "core/hw/gpu.h" #include "core/hw/gpu.h"
#include "video_core/regs_framebuffer.h" #include "video_core/regs_framebuffer.h"
#include "video_core/regs_texturing.h" #include "video_core/regs_texturing.h"
@ -361,6 +362,9 @@ struct CachedSurface : SurfaceParams, std::enable_shared_from_this<CachedSurface
/// level_watchers[i] watches the (i+1)-th level mipmap source surface /// level_watchers[i] watches the (i+1)-th level mipmap source surface
std::array<std::shared_ptr<SurfaceWatcher>, 7> level_watchers; std::array<std::shared_ptr<SurfaceWatcher>, 7> level_watchers;
bool is_custom = false;
Core::CustomTexInfo custom_tex_info;
static constexpr unsigned int GetGLBytesPerPixel(PixelFormat format) { static constexpr unsigned int GetGLBytesPerPixel(PixelFormat format) {
// OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type // OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type
return format == PixelFormat::Invalid return format == PixelFormat::Invalid
@ -377,6 +381,11 @@ struct CachedSurface : SurfaceParams, std::enable_shared_from_this<CachedSurface
void LoadGLBuffer(PAddr load_start, PAddr load_end); void LoadGLBuffer(PAddr load_start, PAddr load_end);
void FlushGLBuffer(PAddr flush_start, PAddr flush_end); void FlushGLBuffer(PAddr flush_start, PAddr flush_end);
// Custom texture loading and dumping
bool LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_info,
Common::Rectangle<u32>& custom_rect);
void DumpTexture(GLuint target_tex, u64 tex_hash);
// Upload/Download data in gl_buffer in/to this surface's texture // Upload/Download data in gl_buffer in/to this surface's texture
void UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint read_fb_handle, void UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint read_fb_handle,
GLuint draw_fb_handle); GLuint draw_fb_handle);