diff --git a/CMakeLists.txt b/CMakeLists.txt index 315bc237d..50153c808 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,7 +140,7 @@ if (ENABLE_SDL2) if (CITRA_USE_BUNDLED_SDL2) # Detect toolchain and platform if ((MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS 1930) AND ARCHITECTURE_x86_64) - set(SDL2_VER "SDL2-2.0.8") + set(SDL2_VER "SDL2-2.0.10") else() message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRA_USE_BUNDLED_SDL2 and provide your own.") endif() diff --git a/CMakeModules/CopyCitraQt5Deps.cmake b/CMakeModules/CopyCitraQt5Deps.cmake index 88f9b8af1..d9014354e 100644 --- a/CMakeModules/CopyCitraQt5Deps.cmake +++ b/CMakeModules/CopyCitraQt5Deps.cmake @@ -6,10 +6,10 @@ function(copy_citra_Qt5_deps target_dir) set(Qt5_MEDIASERVICE_DIR "${Qt5_DIR}/../../../plugins/mediaservice/") set(Qt5_STYLES_DIR "${Qt5_DIR}/../../../plugins/styles/") set(Qt5_IMAGEFORMATS_DIR "${Qt5_DIR}/../../../plugins/imageformats/") - set(PLATFORMS ${DLL_DEST}platforms/) - set(MEDIASERVICE ${DLL_DEST}mediaservice/) - set(STYLES ${DLL_DEST}styles/) - set(IMAGEFORMATS ${DLL_DEST}imageformats/) + set(PLATFORMS ${DLL_DEST}plugins/platforms/) + set(MEDIASERVICE ${DLL_DEST}plugins/mediaservice/) + set(STYLES ${DLL_DEST}plugins/styles/) + set(IMAGEFORMATS ${DLL_DEST}plugins/imageformats/) windows_copy_files(${target_dir} ${Qt5_DLL_DIR} ${DLL_DEST} icudt*.dll icuin*.dll @@ -38,4 +38,10 @@ function(copy_citra_Qt5_deps target_dir) qwbmp$<$:d>.dll qwebp$<$:d>.dll ) + + # Create an empty qt.conf file. Qt will detect that this file exists, and use the folder that its in as the root folder. + # This way it'll look for plugins in the root/plugins/ folder + add_custom_command(TARGET citra-qt POST_BUILD + COMMAND ${CMAKE_COMMAND} -E touch ${DLL_DEST}qt.conf + ) endfunction(copy_citra_Qt5_deps) diff --git a/appveyor.yml b/appveyor.yml index d8cbfc35f..6eb4bccaa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,7 +27,7 @@ install: - ps: | if ($env:BUILD_TYPE -eq 'mingw') { $dependencies = "mingw64/mingw-w64-x86_64-cmake mingw64/mingw-w64-x86_64-qt5 mingw64/mingw-w64-x86_64-ffmpeg" - C:\msys64\usr\bin\bash -lc "pacman --noconfirm -U http://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-SDL2-2.0.5-2-any.pkg.tar.xz" + C:\msys64\usr\bin\bash -lc "pacman --noconfirm -U http://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-SDL2-2.0.10-1-any.pkg.tar.xz" C:\msys64\usr\bin\bash -lc "pacman --noconfirm -S $dependencies" # (HACK) ignore errors 0 diff --git a/src/audio_core/dsp_interface.cpp b/src/audio_core/dsp_interface.cpp index b6e74b82c..1bb2a1d34 100644 --- a/src/audio_core/dsp_interface.cpp +++ b/src/audio_core/dsp_interface.cpp @@ -38,14 +38,14 @@ void DspInterface::EnableStretching(bool enable) { perform_time_stretching = enable; } -void DspInterface::OutputFrame(StereoFrame16& frame) { +void DspInterface::OutputFrame(StereoFrame16 frame) { if (!sink) return; fifo.Push(frame.data(), frame.size()); if (Core::System::GetInstance().VideoDumper().IsDumping()) { - Core::System::GetInstance().VideoDumper().AddAudioFrame(frame); + Core::System::GetInstance().VideoDumper().AddAudioFrame(std::move(frame)); } } @@ -56,7 +56,7 @@ void DspInterface::OutputSample(std::array sample) { fifo.Push(&sample, 1); if (Core::System::GetInstance().VideoDumper().IsDumping()) { - Core::System::GetInstance().VideoDumper().AddAudioSample(sample); + Core::System::GetInstance().VideoDumper().AddAudioSample(std::move(sample)); } } diff --git a/src/audio_core/dsp_interface.h b/src/audio_core/dsp_interface.h index 00adfa3ea..41ab0c0dc 100644 --- a/src/audio_core/dsp_interface.h +++ b/src/audio_core/dsp_interface.h @@ -101,7 +101,7 @@ public: void EnableStretching(bool enable); protected: - void OutputFrame(StereoFrame16& frame); + void OutputFrame(StereoFrame16 frame); void OutputSample(std::array sample); private: diff --git a/src/audio_core/hle/hle.cpp b/src/audio_core/hle/hle.cpp index 3a761dcfa..df52f461b 100644 --- a/src/audio_core/hle/hle.cpp +++ b/src/audio_core/hle/hle.cpp @@ -431,7 +431,7 @@ bool DspHle::Impl::Tick() { // shared memory region) current_frame = GenerateCurrentFrame(); - parent.OutputFrame(current_frame); + parent.OutputFrame(std::move(current_frame)); return true; } diff --git a/src/audio_core/hle/source.cpp b/src/audio_core/hle/source.cpp index c8ee3b618..a4a0f5412 100644 --- a/src/audio_core/hle/source.cpp +++ b/src/audio_core/hle/source.cpp @@ -289,7 +289,9 @@ void Source::GenerateFrame() { break; } } - state.next_sample_number += static_cast(frame_position); + // TODO(jroweboy): Keep track of frame_position independently so that it doesn't lose precision + // over time + state.next_sample_number += static_cast(frame_position * state.rate_multiplier); state.filters.ProcessFrame(current_frame); } diff --git a/src/audio_core/lle/lle.cpp b/src/audio_core/lle/lle.cpp index e9948d1c2..0121bae27 100644 --- a/src/audio_core/lle/lle.cpp +++ b/src/audio_core/lle/lle.cpp @@ -483,7 +483,8 @@ DspLle::DspLle(Memory::MemorySystem& memory, bool multithread) *memory.GetFCRAMPointer(address - Memory::FCRAM_PADDR) = value; }; impl->teakra.SetAHBMCallback(ahbm); - impl->teakra.SetAudioCallback([this](std::array sample) { OutputSample(sample); }); + impl->teakra.SetAudioCallback( + [this](std::array sample) { OutputSample(std::move(sample)); }); } DspLle::~DspLle() = default; diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index ce06b31b7..a1455d373 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -409,7 +409,7 @@ int main(int argc, char** argv) { if (!dump_video.empty()) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; - system.VideoDumper().StartDumping(dump_video, "webm", layout); + system.VideoDumper().StartDumping(dump_video, layout); } std::thread render_thread([&emu_window] { emu_window->Present(); }); diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 47270ab36..400c655ff 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -132,8 +132,6 @@ void Config::ReadValues() { static_cast(sdl2_config->GetInteger("Renderer", "use_vsync_new", 1)); Settings::values.texture_filter_name = sdl2_config->GetString("Renderer", "texture_filter_name", "none"); - Settings::values.texture_filter_factor = - sdl2_config->GetInteger("Renderer", "texture_filter_factor", 1); Settings::values.render_3d = static_cast( sdl2_config->GetInteger("Renderer", "render_3d", 0)); @@ -201,7 +199,7 @@ void Config::ReadValues() { sdl2_config->GetBoolean("Data Storage", "use_virtual_sd", true); // System - Settings::values.is_new_3ds = sdl2_config->GetBoolean("System", "is_new_3ds", false); + Settings::values.is_new_3ds = sdl2_config->GetBoolean("System", "is_new_3ds", true); Settings::values.region_value = sdl2_config->GetInteger("System", "region_value", Settings::REGION_VALUE_AUTO_SELECT); Settings::values.init_clock = @@ -270,6 +268,33 @@ void Config::ReadValues() { sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org"); Settings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", ""); Settings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", ""); + + // Video Dumping + Settings::values.output_format = + sdl2_config->GetString("Video Dumping", "output_format", "webm"); + Settings::values.format_options = sdl2_config->GetString("Video Dumping", "format_options", ""); + + Settings::values.video_encoder = + sdl2_config->GetString("Video Dumping", "video_encoder", "libvpx-vp9"); + + // Options for variable bit rate live streaming taken from here: + // https://developers.google.com/media/vp9/live-encoding + std::string default_video_options; + if (Settings::values.video_encoder == "libvpx-vp9") { + default_video_options = + "quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1"; + } + Settings::values.video_encoder_options = + sdl2_config->GetString("Video Dumping", "video_encoder_options", default_video_options); + Settings::values.video_bitrate = + sdl2_config->GetInteger("Video Dumping", "video_bitrate", 2500000); + + Settings::values.audio_encoder = + sdl2_config->GetString("Video Dumping", "audio_encoder", "libvorbis"); + Settings::values.audio_encoder_options = + sdl2_config->GetString("Video Dumping", "audio_encoder_options", ""); + Settings::values.audio_bitrate = + sdl2_config->GetInteger("Video Dumping", "audio_bitrate", 64000); } void Config::Reload() { diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 881700913..a6f7d5585 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -132,9 +132,8 @@ use_disk_shader_cache = # factor for the 3DS resolution resolution_factor = -# Texture filter name and scale factor +# Texture filter name texture_filter_name = -texture_filter_factor = # Turns on the frame limiter, which will limit frames output to the target game speed # 0: Off, 1: On (default) @@ -244,7 +243,7 @@ use_virtual_sd = [System] # The system model that Citra will try to emulate -# 0: Old 3DS (default), 1: New 3DS +# 0: Old 3DS, 1: New 3DS (default) is_new_3ds = # The system region that Citra will use during emulation @@ -305,5 +304,31 @@ web_api_url = https://api.citra-emu.org # See https://profile.citra-emu.org/ for more info citra_username = citra_token = + +[Video Dumping] +# Format of the video to output, default: webm +output_format = + +# Options passed to the muxer (optional) +# This is a param package, format: [key1]:[value1],[key2]:[value2],... +format_options = + +# Video encoder used, default: libvpx-vp9 +video_encoder = + +# Options passed to the video codec (optional) +video_encoder_options = + +# Video bitrate, default: 2500000 +video_bitrate = + +# Audio encoder used, default: libvorbis +audio_encoder = + +# Options passed to the audio codec (optional) +audio_encoder_options = + +# Audio bitrate, default: 64000 +audio_bitrate = )"; } diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 7ca0398fa..836e3c8ae 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -162,6 +162,20 @@ add_executable(citra-qt util/util.h ) +if (ENABLE_FFMPEG_VIDEO_DUMPER) + target_sources(citra-qt PRIVATE + dumping/dumping_dialog.cpp + dumping/dumping_dialog.h + dumping/dumping_dialog.ui + dumping/option_set_dialog.cpp + dumping/option_set_dialog.h + dumping/option_set_dialog.ui + dumping/options_dialog.cpp + dumping/options_dialog.h + dumping/options_dialog.ui + ) +endif() + file(GLOB COMPAT_LIST ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index feab64906..053d88f51 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -104,8 +104,8 @@ void EmuThread::run() { } OpenGLWindow::OpenGLWindow(QWindow* parent, QWidget* event_handler, QOpenGLContext* shared_context) - : QWindow(parent), event_handler(event_handler), - context(new QOpenGLContext(shared_context->parent())) { + : QWindow(parent), context(new QOpenGLContext(shared_context->parent())), + event_handler(event_handler) { // disable vsync for any shared contexts auto format = shared_context->format(); @@ -201,6 +201,8 @@ GRenderWindow::GRenderWindow(QWidget* parent_, EmuThread* emu_thread) setLayout(layout); InputCommon::Init(); + this->setMouseTracking(true); + GMainWindow* parent = GetMainWindow(); connect(this, &GRenderWindow::FirstFrameDisplayed, parent, &GMainWindow::OnLoadComplete); } @@ -297,6 +299,7 @@ void GRenderWindow::mousePressEvent(QMouseEvent* event) { } else if (event->button() == Qt::RightButton) { InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y()); } + QWidget::mouseMoveEvent(event); } void GRenderWindow::mouseMoveEvent(QMouseEvent* event) { @@ -307,6 +310,7 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) { const auto [x, y] = ScaleTouch(pos); this->TouchMoved(x, y); InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y()); + QWidget::mouseMoveEvent(event); } void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) { diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index d815c99f5..88be0330f 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -95,6 +95,7 @@ void Config::ReadValues() { ReadMiscellaneousValues(); ReadDebuggingValues(); ReadWebServiceValues(); + ReadVideoDumpingValues(); ReadUIValues(); ReadUtilityValues(); } @@ -456,8 +457,6 @@ void Config::ReadRendererValues() { ReadSetting(QStringLiteral("texture_filter_name"), QStringLiteral("none")) .toString() .toStdString(); - Settings::values.texture_filter_factor = - ReadSetting(QStringLiteral("texture_filter_factor"), 1).toInt(); qt_config->endGroup(); } @@ -484,7 +483,7 @@ void Config::ReadShortcutValues() { void Config::ReadSystemValues() { qt_config->beginGroup(QStringLiteral("System")); - Settings::values.is_new_3ds = ReadSetting(QStringLiteral("is_new_3ds"), false).toBool(); + Settings::values.is_new_3ds = ReadSetting(QStringLiteral("is_new_3ds"), true).toBool(); Settings::values.region_value = ReadSetting(QStringLiteral("region_value"), Settings::REGION_VALUE_AUTO_SELECT).toInt(); Settings::values.init_clock = static_cast( @@ -496,6 +495,49 @@ void Config::ReadSystemValues() { qt_config->endGroup(); } +// Options for variable bit rate live streaming taken from here: +// https://developers.google.com/media/vp9/live-encoding +const QString DEFAULT_VIDEO_ENCODER_OPTIONS = + QStringLiteral("quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1"); +const QString DEFAULT_AUDIO_ENCODER_OPTIONS = QString{}; + +void Config::ReadVideoDumpingValues() { + qt_config->beginGroup(QStringLiteral("VideoDumping")); + + Settings::values.output_format = + ReadSetting(QStringLiteral("output_format"), QStringLiteral("webm")) + .toString() + .toStdString(); + Settings::values.format_options = + ReadSetting(QStringLiteral("format_options")).toString().toStdString(); + + Settings::values.video_encoder = + ReadSetting(QStringLiteral("video_encoder"), QStringLiteral("libvpx-vp9")) + .toString() + .toStdString(); + + Settings::values.video_encoder_options = + ReadSetting(QStringLiteral("video_encoder_options"), DEFAULT_VIDEO_ENCODER_OPTIONS) + .toString() + .toStdString(); + + Settings::values.video_bitrate = + ReadSetting(QStringLiteral("video_bitrate"), 2500000).toULongLong(); + + Settings::values.audio_encoder = + ReadSetting(QStringLiteral("audio_encoder"), QStringLiteral("libvorbis")) + .toString() + .toStdString(); + Settings::values.audio_encoder_options = + ReadSetting(QStringLiteral("audio_encoder_options"), DEFAULT_AUDIO_ENCODER_OPTIONS) + .toString() + .toStdString(); + Settings::values.audio_bitrate = + ReadSetting(QStringLiteral("audio_bitrate"), 64000).toULongLong(); + + qt_config->endGroup(); +} + void Config::ReadUIValues() { qt_config->beginGroup(QStringLiteral("UI")); @@ -530,6 +572,8 @@ void Config::ReadUIValues() { UISettings::values.show_console = ReadSetting(QStringLiteral("showConsole"), false).toBool(); UISettings::values.pause_when_in_background = ReadSetting(QStringLiteral("pauseWhenInBackground"), false).toBool(); + UISettings::values.hide_mouse = + ReadSetting(QStringLiteral("hideInactiveMouse"), false).toBool(); qt_config->endGroup(); } @@ -628,6 +672,7 @@ void Config::SaveValues() { SaveMiscellaneousValues(); SaveDebuggingValues(); SaveWebServiceValues(); + SaveVideoDumpingValues(); SaveUIValues(); SaveUtilityValues(); } @@ -895,8 +940,6 @@ void Config::SaveRendererValues() { WriteSetting(QStringLiteral("texture_filter_name"), QString::fromStdString(Settings::values.texture_filter_name), QStringLiteral("none")); - WriteSetting(QStringLiteral("texture_filter_factor"), Settings::values.texture_filter_factor, - 1); qt_config->endGroup(); } @@ -923,7 +966,7 @@ void Config::SaveShortcutValues() { void Config::SaveSystemValues() { qt_config->beginGroup(QStringLiteral("System")); - WriteSetting(QStringLiteral("is_new_3ds"), Settings::values.is_new_3ds, false); + WriteSetting(QStringLiteral("is_new_3ds"), Settings::values.is_new_3ds, true); WriteSetting(QStringLiteral("region_value"), Settings::values.region_value, Settings::REGION_VALUE_AUTO_SELECT); WriteSetting(QStringLiteral("init_clock"), static_cast(Settings::values.init_clock), @@ -934,6 +977,33 @@ void Config::SaveSystemValues() { qt_config->endGroup(); } +void Config::SaveVideoDumpingValues() { + qt_config->beginGroup(QStringLiteral("VideoDumping")); + + WriteSetting(QStringLiteral("output_format"), + QString::fromStdString(Settings::values.output_format), QStringLiteral("webm")); + WriteSetting(QStringLiteral("format_options"), + QString::fromStdString(Settings::values.format_options)); + WriteSetting(QStringLiteral("video_encoder"), + QString::fromStdString(Settings::values.video_encoder), + QStringLiteral("libvpx-vp9")); + WriteSetting(QStringLiteral("video_encoder_options"), + QString::fromStdString(Settings::values.video_encoder_options), + DEFAULT_VIDEO_ENCODER_OPTIONS); + WriteSetting(QStringLiteral("video_bitrate"), + static_cast(Settings::values.video_bitrate), 2500000); + WriteSetting(QStringLiteral("audio_encoder"), + QString::fromStdString(Settings::values.audio_encoder), + QStringLiteral("libvorbis")); + WriteSetting(QStringLiteral("audio_encoder_options"), + QString::fromStdString(Settings::values.audio_encoder_options), + DEFAULT_AUDIO_ENCODER_OPTIONS); + WriteSetting(QStringLiteral("audio_bitrate"), + static_cast(Settings::values.audio_bitrate), 64000); + + qt_config->endGroup(); +} + void Config::SaveUIValues() { qt_config->beginGroup(QStringLiteral("UI")); @@ -962,6 +1032,7 @@ void Config::SaveUIValues() { WriteSetting(QStringLiteral("showConsole"), UISettings::values.show_console, false); WriteSetting(QStringLiteral("pauseWhenInBackground"), UISettings::values.pause_when_in_background, false); + WriteSetting(QStringLiteral("hideInactiveMouse"), UISettings::values.hide_mouse, false); qt_config->endGroup(); } diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 7d65d41b6..8b1cf8193 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -44,6 +44,7 @@ private: void ReadUpdaterValues(); void ReadUtilityValues(); void ReadWebServiceValues(); + void ReadVideoDumpingValues(); void SaveValues(); void SaveAudioValues(); @@ -65,6 +66,7 @@ private: void SaveUpdaterValues(); void SaveUtilityValues(); void SaveWebServiceValues(); + void SaveVideoDumpingValues(); QVariant ReadSetting(const QString& name) const; QVariant ReadSetting(const QString& name, const QVariant& default_value) const; diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp index e1c6b6bdd..f25b9c1e1 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -8,17 +8,14 @@ #include "core/settings.h" #include "ui_configure_enhancements.h" #include "video_core/renderer_opengl/post_processing_opengl.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" ConfigureEnhancements::ConfigureEnhancements(QWidget* parent) : QWidget(parent), ui(new Ui::ConfigureEnhancements) { ui->setupUi(this); - for (const auto& filter : OpenGL::TextureFilterManager::TextureFilterMap()) - ui->texture_filter_combobox->addItem(QString::fromStdString(filter.first.data())); - - connect(ui->texture_filter_combobox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ConfigureEnhancements::updateTextureFilter); + for (const auto& filter : OpenGL::TextureFilterer::GetFilterNames()) + ui->texture_filter_combobox->addItem(QString::fromStdString(filter.data())); SetConfiguration(); @@ -60,7 +57,6 @@ void ConfigureEnhancements::SetConfiguration() { ui->factor_3d->setValue(Settings::values.factor_3d); updateShaders(Settings::values.render_3d); ui->toggle_linear_filter->setChecked(Settings::values.filter_mode); - ui->texture_scale_spinbox->setValue(Settings::values.texture_filter_factor); int tex_filter_idx = ui->texture_filter_combobox->findText( QString::fromStdString(Settings::values.texture_filter_name)); if (tex_filter_idx == -1) { @@ -68,7 +64,6 @@ void ConfigureEnhancements::SetConfiguration() { } else { ui->texture_filter_combobox->setCurrentIndex(tex_filter_idx); } - updateTextureFilter(tex_filter_idx); ui->layout_combobox->setCurrentIndex(static_cast(Settings::values.layout_option)); ui->swap_screen->setChecked(Settings::values.swap_screen); ui->toggle_disk_shader_cache->setChecked(Settings::values.use_hw_shader && @@ -105,17 +100,6 @@ void ConfigureEnhancements::updateShaders(Settings::StereoRenderOption stereo_op } } -void ConfigureEnhancements::updateTextureFilter(int index) { - if (index == -1) - return; - ui->texture_filter_group->setEnabled(index != 0); - const auto& clamp = OpenGL::TextureFilterManager::TextureFilterMap() - .at(ui->texture_filter_combobox->currentText().toStdString()) - .clamp_scale; - ui->texture_scale_spinbox->setMinimum(clamp.min); - ui->texture_scale_spinbox->setMaximum(clamp.max); -} - void ConfigureEnhancements::RetranslateUI() { ui->retranslateUi(this); } @@ -130,7 +114,6 @@ void ConfigureEnhancements::ApplyConfiguration() { ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString(); Settings::values.filter_mode = ui->toggle_linear_filter->isChecked(); Settings::values.texture_filter_name = ui->texture_filter_combobox->currentText().toStdString(); - Settings::values.texture_filter_factor = ui->texture_scale_spinbox->value(); Settings::values.layout_option = static_cast(ui->layout_combobox->currentIndex()); Settings::values.swap_screen = ui->swap_screen->isChecked(); diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui index d3193e9eb..909a98643 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -131,42 +131,6 @@ - - - - - 16 - - - 0 - - - 0 - - - 0 - - - - - - - Texture Scale Factor - - - - - - - 1 - - - - - - - - diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index 47d559e3d..419331d13 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -27,6 +27,7 @@ ConfigureGeneral::~ConfigureGeneral() = default; void ConfigureGeneral::SetConfiguration() { ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); ui->toggle_background_pause->setChecked(UISettings::values.pause_when_in_background); + ui->toggle_hide_mouse->setChecked(UISettings::values.hide_mouse); ui->toggle_update_check->setChecked(UISettings::values.check_for_update_on_start); ui->toggle_auto_update->setChecked(UISettings::values.update_on_close); @@ -55,6 +56,7 @@ void ConfigureGeneral::ResetDefaults() { void ConfigureGeneral::ApplyConfiguration() { UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); UISettings::values.pause_when_in_background = ui->toggle_background_pause->isChecked(); + UISettings::values.hide_mouse = ui->toggle_hide_mouse->isChecked(); UISettings::values.check_for_update_on_start = ui->toggle_update_check->isChecked(); UISettings::values.update_on_close = ui->toggle_auto_update->isChecked(); diff --git a/src/citra_qt/configuration/configure_general.ui b/src/citra_qt/configuration/configure_general.ui index 181455a64..6b2aa4416 100644 --- a/src/citra_qt/configuration/configure_general.ui +++ b/src/citra_qt/configuration/configure_general.ui @@ -36,6 +36,13 @@ + + + + Hide mouse on inactivity + + + diff --git a/src/citra_qt/configuration/configure_motion_touch.cpp b/src/citra_qt/configuration/configure_motion_touch.cpp index c1961d71e..a5457cdf7 100644 --- a/src/citra_qt/configuration/configure_motion_touch.cpp +++ b/src/citra_qt/configuration/configure_motion_touch.cpp @@ -30,7 +30,7 @@ CalibrationConfigurationDialog::CalibrationConfigurationDialog(QWidget* parent, setLayout(layout); using namespace InputCommon::CemuhookUDP; - job = std::move(std::make_unique( + job = std::make_unique( host, port, pad_index, client_id, [this](CalibrationConfigurationJob::Status status) { QString text; @@ -56,7 +56,7 @@ CalibrationConfigurationDialog::CalibrationConfigurationDialog(QWidget* parent, min_y = min_y_; max_x = max_x_; max_y = max_y_; - })); + }); } CalibrationConfigurationDialog::~CalibrationConfigurationDialog() = default; diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp index 139d92f7b..32ea7b3f3 100644 --- a/src/citra_qt/configuration/configure_system.cpp +++ b/src/citra_qt/configuration/configure_system.cpp @@ -277,6 +277,8 @@ void ConfigureSystem::SetConfiguration() { ui->slider_clock_speed->setValue(SettingsToSlider(Settings::values.cpu_clock_percentage)); ui->clock_display_label->setText( QStringLiteral("%1%").arg(Settings::values.cpu_clock_percentage)); + + ui->toggle_new_3ds->setChecked(Settings::values.is_new_3ds); } void ConfigureSystem::ReadSystemSettings() { @@ -374,6 +376,8 @@ void ConfigureSystem::ApplyConfiguration() { Settings::values.init_clock = static_cast(ui->combo_init_clock->currentIndex()); Settings::values.init_time = ui->edit_init_time->dateTime().toTime_t(); + + Settings::values.is_new_3ds = ui->toggle_new_3ds->isChecked(); } Settings::values.cpu_clock_percentage = SliderToSettings(ui->slider_clock_speed->value()); diff --git a/src/citra_qt/configuration/configure_system.ui b/src/citra_qt/configuration/configure_system.ui index 554993990..c6f12060c 100644 --- a/src/citra_qt/configuration/configure_system.ui +++ b/src/citra_qt/configuration/configure_system.ui @@ -22,14 +22,74 @@ System Settings - - - - Username + + + + Note: this can be overridden when region setting is auto-select + + + Japanese (日本語) + + + + + English + + + + + French (français) + + + + + German (Deutsch) + + + + + Italian (italiano) + + + + + Spanish (español) + + + + + Simplified Chinese (简体中文) + + + + + Korean (한국어) + + + + + Dutch (Nederlands) + + + + + Portuguese (português) + + + + + Russian (Русский) + + + + + Traditional Chinese (正體中文) + + - + @@ -42,14 +102,33 @@ - - + + - Birthday + Username - + + + + + Mono + + + + + Stereo + + + + + Surround + + + + + @@ -120,124 +199,38 @@ - + Language - - - - Note: this can be overridden when region setting is auto-select + + + + Birthday - - - Japanese (日本語) - - - - - English - - - - - French (français) - - - - - German (Deutsch) - - - - - Italian (italiano) - - - - - Spanish (español) - - - - - Simplified Chinese (简体中文) - - - - - Korean (한국어) - - - - - Dutch (Nederlands) - - - - - Portuguese (português) - - - - - Russian (Русский) - - - - - Traditional Chinese (正體中文) - - - + Sound output mode - - - - - Mono - - - - - Stereo - - - - - Surround - - - + + - + Country - - - - - - - Clock - - - - + @@ -251,42 +244,35 @@ - + + + + Clock + + + + Startup time - - - - yyyy-MM-ddTHH:mm:ss - - - - - - - Play Coins: - - - - + 300 - - + + - Console ID: + Play Coins: - + @@ -302,6 +288,27 @@ + + + + Console ID: + + + + + + + yyyy-MM-ddTHH:mm:ss + + + + + + + Enable New 3DS mode + + + diff --git a/src/citra_qt/dumping/dumping_dialog.cpp b/src/citra_qt/dumping/dumping_dialog.cpp new file mode 100644 index 000000000..70d165c01 --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.cpp @@ -0,0 +1,220 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "citra_qt/dumping/dumping_dialog.h" +#include "citra_qt/dumping/options_dialog.h" +#include "citra_qt/uisettings.h" +#include "core/settings.h" +#include "ui_dumping_dialog.h" + +DumpingDialog::DumpingDialog(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + + ui->setupUi(this); + + format_generic_options = VideoDumper::GetFormatGenericOptions(); + encoder_generic_options = VideoDumper::GetEncoderGenericOptions(); + + connect(ui->pathExplore, &QToolButton::clicked, this, &DumpingDialog::OnToolButtonClicked); + connect(ui->buttonBox, &QDialogButtonBox::accepted, [this] { + if (ui->pathLineEdit->text().isEmpty()) { + QMessageBox::critical(this, tr("Citra"), tr("Please specify the output path.")); + return; + } + ApplyConfiguration(); + accept(); + }); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &DumpingDialog::reject); + connect(ui->formatOptionsButton, &QToolButton::clicked, [this] { + OpenOptionsDialog(formats.at(ui->formatComboBox->currentData().toUInt()).options, + format_generic_options, ui->formatOptionsLineEdit); + }); + connect(ui->videoEncoderOptionsButton, &QToolButton::clicked, [this] { + OpenOptionsDialog( + video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).options, + encoder_generic_options, ui->videoEncoderOptionsLineEdit); + }); + connect(ui->audioEncoderOptionsButton, &QToolButton::clicked, [this] { + OpenOptionsDialog( + audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).options, + encoder_generic_options, ui->audioEncoderOptionsLineEdit); + }); + + SetConfiguration(); + + connect(ui->formatComboBox, qOverload(&QComboBox::currentIndexChanged), [this] { + ui->pathLineEdit->setText(QString{}); + ui->formatOptionsLineEdit->clear(); + PopulateEncoders(); + }); + + connect(ui->videoEncoderComboBox, qOverload(&QComboBox::currentIndexChanged), + [this] { ui->videoEncoderOptionsLineEdit->clear(); }); + connect(ui->audioEncoderComboBox, qOverload(&QComboBox::currentIndexChanged), + [this] { ui->audioEncoderOptionsLineEdit->clear(); }); +} + +DumpingDialog::~DumpingDialog() = default; + +QString DumpingDialog::GetFilePath() const { + return ui->pathLineEdit->text(); +} + +void DumpingDialog::Populate() { + formats = VideoDumper::ListFormats(); + video_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_VIDEO); + audio_encoders = VideoDumper::ListEncoders(AVMEDIA_TYPE_AUDIO); + + // Check that these are not empty + QString missing; + if (formats.empty()) { + missing = tr("output formats"); + } + if (video_encoders.empty()) { + missing = tr("video encoders"); + } + if (audio_encoders.empty()) { + missing = tr("audio encoders"); + } + + if (!missing.isEmpty()) { + QMessageBox::critical(this, tr("Citra"), + tr("Could not find any available %1.\nPlease check your FFmpeg " + "installation used for compilation.") + .arg(missing)); + reject(); + return; + } + + // Populate formats + for (std::size_t i = 0; i < formats.size(); ++i) { + const auto& format = formats[i]; + + // Check format: only formats that have video encoders and audio encoders are displayed + bool has_video = false; + for (const auto& video_encoder : video_encoders) { + if (format.supported_video_codecs.count(video_encoder.codec)) { + has_video = true; + break; + } + } + if (!has_video) + continue; + + bool has_audio = false; + for (const auto& audio_encoder : audio_encoders) { + if (format.supported_audio_codecs.count(audio_encoder.codec)) { + has_audio = true; + break; + } + } + if (!has_audio) + continue; + + ui->formatComboBox->addItem(tr("%1 (%2)").arg(QString::fromStdString(format.long_name), + QString::fromStdString(format.name)), + static_cast(i)); + if (format.name == Settings::values.output_format) { + ui->formatComboBox->setCurrentIndex(ui->formatComboBox->count() - 1); + } + } + PopulateEncoders(); +} + +void DumpingDialog::PopulateEncoders() { + const auto& format = formats.at(ui->formatComboBox->currentData().toUInt()); + + ui->videoEncoderComboBox->clear(); + for (std::size_t i = 0; i < video_encoders.size(); ++i) { + const auto& video_encoder = video_encoders[i]; + if (!format.supported_video_codecs.count(video_encoder.codec)) { + continue; + } + + ui->videoEncoderComboBox->addItem( + tr("%1 (%2)").arg(QString::fromStdString(video_encoder.long_name), + QString::fromStdString(video_encoder.name)), + static_cast(i)); + if (video_encoder.name == Settings::values.video_encoder) { + ui->videoEncoderComboBox->setCurrentIndex(ui->videoEncoderComboBox->count() - 1); + } + } + + ui->audioEncoderComboBox->clear(); + for (std::size_t i = 0; i < audio_encoders.size(); ++i) { + const auto& audio_encoder = audio_encoders[i]; + if (!format.supported_audio_codecs.count(audio_encoder.codec)) { + continue; + } + + ui->audioEncoderComboBox->addItem( + tr("%1 (%2)").arg(QString::fromStdString(audio_encoder.long_name), + QString::fromStdString(audio_encoder.name)), + static_cast(i)); + if (audio_encoder.name == Settings::values.audio_encoder) { + ui->audioEncoderComboBox->setCurrentIndex(ui->audioEncoderComboBox->count() - 1); + } + } +} + +void DumpingDialog::OnToolButtonClicked() { + const auto& format = formats.at(ui->formatComboBox->currentData().toUInt()); + + QString extensions; + for (const auto& ext : format.extensions) { + if (!extensions.isEmpty()) { + extensions.append(QLatin1Char{' '}); + } + extensions.append(QStringLiteral("*.%1").arg(QString::fromStdString(ext))); + } + + const auto path = QFileDialog::getSaveFileName( + this, tr("Select Video Output Path"), last_path, + tr("%1 (%2)").arg(QString::fromStdString(format.long_name), extensions)); + if (!path.isEmpty()) { + last_path = QFileInfo(ui->pathLineEdit->text()).path(); + ui->pathLineEdit->setText(path); + } +} + +void DumpingDialog::OpenOptionsDialog(const std::vector& specific_options, + const std::vector& generic_options, + QLineEdit* line_edit) { + OptionsDialog dialog(this, specific_options, generic_options, line_edit->text().toStdString()); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + return; + } + + line_edit->setText(QString::fromStdString(dialog.GetCurrentValue())); +} + +void DumpingDialog::SetConfiguration() { + Populate(); + + ui->formatOptionsLineEdit->setText(QString::fromStdString(Settings::values.format_options)); + ui->videoEncoderOptionsLineEdit->setText( + QString::fromStdString(Settings::values.video_encoder_options)); + ui->audioEncoderOptionsLineEdit->setText( + QString::fromStdString(Settings::values.audio_encoder_options)); + last_path = UISettings::values.video_dumping_path; + ui->videoBitrateSpinBox->setValue(static_cast(Settings::values.video_bitrate)); + ui->audioBitrateSpinBox->setValue(static_cast(Settings::values.audio_bitrate)); +} + +void DumpingDialog::ApplyConfiguration() { + Settings::values.output_format = formats.at(ui->formatComboBox->currentData().toUInt()).name; + Settings::values.format_options = ui->formatOptionsLineEdit->text().toStdString(); + Settings::values.video_encoder = + video_encoders.at(ui->videoEncoderComboBox->currentData().toUInt()).name; + Settings::values.video_encoder_options = ui->videoEncoderOptionsLineEdit->text().toStdString(); + Settings::values.video_bitrate = ui->videoBitrateSpinBox->value(); + Settings::values.audio_encoder = + audio_encoders.at(ui->audioEncoderComboBox->currentData().toUInt()).name; + Settings::values.audio_encoder_options = ui->audioEncoderOptionsLineEdit->text().toStdString(); + Settings::values.audio_bitrate = ui->audioBitrateSpinBox->value(); + UISettings::values.video_dumping_path = last_path; + Settings::Apply(); +} diff --git a/src/citra_qt/dumping/dumping_dialog.h b/src/citra_qt/dumping/dumping_dialog.h new file mode 100644 index 000000000..284f215c3 --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.h @@ -0,0 +1,43 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "core/dumping/ffmpeg_backend.h" + +namespace Ui { +class DumpingDialog; +} + +class QLineEdit; + +class DumpingDialog : public QDialog { + Q_OBJECT + +public: + explicit DumpingDialog(QWidget* parent); + ~DumpingDialog() override; + + QString GetFilePath() const; + void ApplyConfiguration(); + +private: + void Populate(); + void PopulateEncoders(); + void SetConfiguration(); + void OnToolButtonClicked(); + void OpenOptionsDialog(const std::vector& specific_options, + const std::vector& generic_options, + QLineEdit* line_edit); + + std::unique_ptr ui; + + QString last_path; + + std::vector formats; + std::vector format_generic_options; + std::vector video_encoders; + std::vector audio_encoders; + std::vector encoder_generic_options; +}; diff --git a/src/citra_qt/dumping/dumping_dialog.ui b/src/citra_qt/dumping/dumping_dialog.ui new file mode 100644 index 000000000..6e33d47d8 --- /dev/null +++ b/src/citra_qt/dumping/dumping_dialog.ui @@ -0,0 +1,213 @@ + + + DumpingDialog + + + + 0 + 0 + 600 + 420 + + + + Dump Video + + + + + + Output + + + + + + Format: + + + + + + + + + + Options: + + + + + + + + + + ... + + + + + + + Path: + + + + + + + + + + ... + + + + + + + + + + Video + + + + + + Encoder: + + + + + + + + 0 + 0 + + + + + + + + Options: + + + + + + + + + + ... + + + + + + + Bitrate: + + + + + + + 10000000 + + + 1000 + + + + + + + bps + + + + + + + + + + Audio + + + + + + Encoder: + + + + + + + + 0 + 0 + + + + + + + + Options: + + + + + + + + + + ... + + + + + + + Bitrate: + + + + + + + 1000000 + + + 100 + + + + + + + bps + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + diff --git a/src/citra_qt/dumping/option_set_dialog.cpp b/src/citra_qt/dumping/option_set_dialog.cpp new file mode 100644 index 000000000..8dab0505e --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.cpp @@ -0,0 +1,299 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "citra_qt/dumping/option_set_dialog.h" +#include "common/logging/log.h" +#include "common/string_util.h" +#include "ui_option_set_dialog.h" + +extern "C" { +#include +} + +static const std::unordered_map TypeNameMap{{ + {AV_OPT_TYPE_BOOL, QT_TR_NOOP("boolean")}, + {AV_OPT_TYPE_FLAGS, QT_TR_NOOP("flags")}, + {AV_OPT_TYPE_DURATION, QT_TR_NOOP("duration")}, + {AV_OPT_TYPE_INT, QT_TR_NOOP("int")}, + {AV_OPT_TYPE_UINT64, QT_TR_NOOP("uint64")}, + {AV_OPT_TYPE_INT64, QT_TR_NOOP("int64")}, + {AV_OPT_TYPE_DOUBLE, QT_TR_NOOP("double")}, + {AV_OPT_TYPE_FLOAT, QT_TR_NOOP("float")}, + {AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("rational")}, + {AV_OPT_TYPE_PIXEL_FMT, QT_TR_NOOP("pixel format")}, + {AV_OPT_TYPE_SAMPLE_FMT, QT_TR_NOOP("sample format")}, + {AV_OPT_TYPE_COLOR, QT_TR_NOOP("color")}, + {AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("image size")}, + {AV_OPT_TYPE_STRING, QT_TR_NOOP("string")}, + {AV_OPT_TYPE_DICT, QT_TR_NOOP("dictionary")}, + {AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("video rate")}, + {AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("channel layout")}, +}}; + +static const std::unordered_map TypeDescriptionMap{{ + {AV_OPT_TYPE_DURATION, QT_TR_NOOP("[<hours (integer)>:][<minutes (integer):]<seconds " + "(decimal)> e.g. 03:00.5 (3min 500ms)")}, + {AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("<num>/<den>")}, + {AV_OPT_TYPE_COLOR, QT_TR_NOOP("0xRRGGBBAA")}, + {AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("<width>x<height>, or preset values like 'vga'.")}, + {AV_OPT_TYPE_DICT, + QT_TR_NOOP("Comma-splitted list of <key>=<value>. Do not put spaces.")}, + {AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("<num>/<den>, or preset values like 'pal'.")}, + {AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("Hexadecimal channel layout mask starting with '0x'.")}, +}}; + +/// Get the preset values of an option. returns {display value, real value} +std::vector> GetPresetValues(const VideoDumper::OptionInfo& option) { + switch (option.type) { + case AV_OPT_TYPE_BOOL: { + return {{QObject::tr("auto"), QStringLiteral("auto")}, + {QObject::tr("true"), QStringLiteral("true")}, + {QObject::tr("false"), QStringLiteral("false")}}; + } + case AV_OPT_TYPE_PIXEL_FMT: { + std::vector> out{{QObject::tr("none"), QStringLiteral("none")}}; + // List all pixel formats + const AVPixFmtDescriptor* current = nullptr; + while ((current = av_pix_fmt_desc_next(current))) { + out.emplace_back(QString::fromUtf8(current->name), QString::fromUtf8(current->name)); + } + return out; + } + case AV_OPT_TYPE_SAMPLE_FMT: { + std::vector> out{{QObject::tr("none"), QStringLiteral("none")}}; + // List all sample formats + int current = 0; + while (true) { + const char* name = av_get_sample_fmt_name(static_cast(current)); + if (name == nullptr) + break; + out.emplace_back(QString::fromUtf8(name), QString::fromUtf8(name)); + } + return out; + } + case AV_OPT_TYPE_INT: + case AV_OPT_TYPE_INT64: + case AV_OPT_TYPE_UINT64: { + std::vector> out; + // Add in all named constants + for (const auto& constant : option.named_constants) { + out.emplace_back(QObject::tr("%1 (0x%2)") + .arg(QString::fromStdString(constant.name)) + .arg(constant.value, 0, 16), + QString::fromStdString(constant.name)); + } + return out; + } + default: + return {}; + } +} + +void OptionSetDialog::InitializeUI(const std::string& initial_value) { + const QString type_name = + TypeNameMap.count(option.type) ? tr(TypeNameMap.at(option.type)) : tr("unknown"); + ui->nameLabel->setText(tr("%1 <%2> %3") + .arg(QString::fromStdString(option.name), type_name, + QString::fromStdString(option.description))); + if (TypeDescriptionMap.count(option.type)) { + ui->formatLabel->setVisible(true); + ui->formatLabel->setText(tr(TypeDescriptionMap.at(option.type))); + } + + if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || + option.type == AV_OPT_TYPE_UINT64 || option.type == AV_OPT_TYPE_FLOAT || + option.type == AV_OPT_TYPE_DOUBLE || option.type == AV_OPT_TYPE_DURATION || + option.type == AV_OPT_TYPE_RATIONAL) { // scalar types + + ui->formatLabel->setVisible(true); + if (!ui->formatLabel->text().isEmpty()) { + ui->formatLabel->text().append(QStringLiteral("\n")); + } + ui->formatLabel->setText( + ui->formatLabel->text().append(tr("Range: %1 - %2").arg(option.min).arg(option.max))); + } + + // Decide and initialize layout + if (option.type == AV_OPT_TYPE_BOOL || option.type == AV_OPT_TYPE_PIXEL_FMT || + option.type == AV_OPT_TYPE_SAMPLE_FMT || + ((option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || + option.type == AV_OPT_TYPE_UINT64) && + !option.named_constants.empty())) { // Use the combobox layout + + layout_type = 1; + ui->comboBox->setVisible(true); + ui->comboBoxHelpLabel->setVisible(true); + + QString real_initial_value = QString::fromStdString(initial_value); + if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || + option.type == AV_OPT_TYPE_UINT64) { + + // Get the name of the initial value + try { + s64 initial_value_integer = std::stoll(initial_value, nullptr, 0); + for (const auto& constant : option.named_constants) { + if (constant.value == initial_value_integer) { + real_initial_value = QString::fromStdString(constant.name); + break; + } + } + } catch (...) { + // Not convertible to integer, ignore + } + } + + bool found = false; + for (const auto& [display, value] : GetPresetValues(option)) { + ui->comboBox->addItem(display, value); + if (value == real_initial_value) { + found = true; + ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1); + } + } + ui->comboBox->addItem(tr("custom")); + + if (!found) { + ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1); + ui->lineEdit->setText(QString::fromStdString(initial_value)); + } + + UpdateUIDisplay(); + + connect(ui->comboBox, &QComboBox::currentTextChanged, this, + &OptionSetDialog::UpdateUIDisplay); + } else if (option.type == AV_OPT_TYPE_FLAGS && + !option.named_constants.empty()) { // Use the check boxes layout + + layout_type = 2; + + for (const auto& constant : option.named_constants) { + auto* checkBox = new QCheckBox(tr("%1 (0x%2) %3") + .arg(QString::fromStdString(constant.name)) + .arg(constant.value, 0, 16) + .arg(QString::fromStdString(constant.description))); + checkBox->setProperty("value", static_cast(constant.value)); + checkBox->setProperty("name", QString::fromStdString(constant.name)); + ui->checkBoxLayout->addWidget(checkBox); + } + SetCheckBoxDefaults(initial_value); + } else { // Use the line edit layout + layout_type = 0; + ui->lineEdit->setVisible(true); + ui->lineEdit->setText(QString::fromStdString(initial_value)); + } + + adjustSize(); +} + +void OptionSetDialog::SetCheckBoxDefaults(const std::string& initial_value) { + if (initial_value.size() >= 2 && + (initial_value.substr(0, 2) == "0x" || initial_value.substr(0, 2) == "0X")) { + // This is a hex mask + try { + u64 value = std::stoull(initial_value, nullptr, 16); + for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { + auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); + if (checkBox) { + checkBox->setChecked(value & checkBox->property("value").toULongLong()); + } + } + } catch (...) { + LOG_ERROR(Frontend, "Could not convert {} to number", initial_value); + } + } else { + // This is a combination of constants, splitted with + or | + std::vector tmp; + Common::SplitString(initial_value, '+', tmp); + + std::vector out; + std::vector tmp2; + for (const auto& str : tmp) { + Common::SplitString(str, '|', tmp2); + out.insert(out.end(), tmp2.begin(), tmp2.end()); + } + for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { + auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); + if (checkBox) { + checkBox->setChecked( + std::find(out.begin(), out.end(), + checkBox->property("name").toString().toStdString()) != out.end()); + } + } + } +} + +void OptionSetDialog::UpdateUIDisplay() { + if (layout_type != 1) + return; + + if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { // custom + ui->comboBoxHelpLabel->setVisible(false); + ui->lineEdit->setVisible(true); + adjustSize(); + return; + } + + ui->lineEdit->setVisible(false); + for (const auto& constant : option.named_constants) { + if (constant.name == ui->comboBox->currentData().toString().toStdString()) { + ui->comboBoxHelpLabel->setVisible(true); + ui->comboBoxHelpLabel->setText(QString::fromStdString(constant.description)); + return; + } + } +} + +std::pair OptionSetDialog::GetCurrentValue() { + if (!is_set) { + return {}; + } + + switch (layout_type) { + case 0: // line edit layout + return {true, ui->lineEdit->text().toStdString()}; + case 1: // combo box layout + if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { + return {true, ui->lineEdit->text().toStdString()}; // custom + } + return {true, ui->comboBox->currentData().toString().toStdString()}; + case 2: { // check boxes layout + std::string out; + for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { + auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); + if (checkBox && checkBox->isChecked()) { + if (!out.empty()) { + out.append("+"); + } + out.append(checkBox->property("name").toString().toStdString()); + } + } + if (out.empty()) { + out = "0x0"; + } + return {true, out}; + } + default: + return {}; + } +} + +OptionSetDialog::OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option_, + const std::string& initial_value) + : QDialog(parent), ui(std::make_unique()), option(std::move(option_)) { + + ui->setupUi(this); + InitializeUI(initial_value); + + connect(ui->unsetButton, &QPushButton::clicked, [this] { + is_set = false; + accept(); + }); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionSetDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionSetDialog::reject); +} + +OptionSetDialog::~OptionSetDialog() = default; diff --git a/src/citra_qt/dumping/option_set_dialog.h b/src/citra_qt/dumping/option_set_dialog.h new file mode 100644 index 000000000..2c5d378d0 --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.h @@ -0,0 +1,33 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "core/dumping/ffmpeg_backend.h" + +namespace Ui { +class OptionSetDialog; +} + +class OptionSetDialog : public QDialog { + Q_OBJECT + +public: + explicit OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option, + const std::string& initial_value); + ~OptionSetDialog() override; + + // {is_set, value} + std::pair GetCurrentValue(); + +private: + void InitializeUI(const std::string& initial_value); + void SetCheckBoxDefaults(const std::string& initial_value); + void UpdateUIDisplay(); + + std::unique_ptr ui; + VideoDumper::OptionInfo option; + bool is_set = true; + int layout_type = -1; // 0 - line edit, 1 - combo box, 2 - flags (check boxes) +}; diff --git a/src/citra_qt/dumping/option_set_dialog.ui b/src/citra_qt/dumping/option_set_dialog.ui new file mode 100644 index 000000000..dcf4bb572 --- /dev/null +++ b/src/citra_qt/dumping/option_set_dialog.ui @@ -0,0 +1,89 @@ + + + OptionSetDialog + + + + 0 + 0 + 600 + 150 + + + + Options + + + + + + + + + false + + + + + + + + + false + + + + + + + false + + + + + + + + + false + + + + + + + + + + Qt::Vertical + + + + + + + + + Unset + + + + + + + Qt::Horizontal + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/src/citra_qt/dumping/options_dialog.cpp b/src/citra_qt/dumping/options_dialog.cpp new file mode 100644 index 000000000..e75fc61c0 --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.cpp @@ -0,0 +1,68 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "citra_qt/dumping/option_set_dialog.h" +#include "citra_qt/dumping/options_dialog.h" +#include "ui_options_dialog.h" + +constexpr char UNSET_TEXT[] = QT_TR_NOOP("[not set]"); + +void OptionsDialog::PopulateOptions() { + const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options; + ui->main->clear(); + ui->main->setSortingEnabled(false); + for (std::size_t i = 0; i < options.size(); ++i) { + const auto& option = options.at(i); + auto* item = new QTreeWidgetItem( + {QString::fromStdString(option.name), QString::fromStdString(current_values.Get( + option.name, tr(UNSET_TEXT).toStdString()))}); + item->setData(1, Qt::UserRole, static_cast(i)); // ID + ui->main->addTopLevelItem(item); + } + ui->main->setSortingEnabled(true); + ui->main->sortItems(0, Qt::AscendingOrder); +} + +void OptionsDialog::OnSetOptionValue(QTreeWidgetItem* item) { + const auto& options = ui->specificRadioButton->isChecked() ? specific_options : generic_options; + const int id = item->data(1, Qt::UserRole).toInt(); + OptionSetDialog dialog(this, options[id], + current_values.Get(options[id].name, options[id].default_value)); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + return; + } + + const auto& [is_set, value] = dialog.GetCurrentValue(); + if (is_set) { + current_values.Set(options[id].name, value); + } else { + current_values.Erase(options[id].name); + } + item->setText(1, is_set ? QString::fromStdString(value) : tr(UNSET_TEXT)); +} + +std::string OptionsDialog::GetCurrentValue() const { + return current_values.Serialize(); +} + +OptionsDialog::OptionsDialog(QWidget* parent, + std::vector specific_options_, + std::vector generic_options_, + const std::string& current_value) + : QDialog(parent), ui(std::make_unique()), + specific_options(std::move(specific_options_)), generic_options(std::move(generic_options_)), + current_values(current_value) { + + ui->setupUi(this); + PopulateOptions(); + + connect(ui->main, &QTreeWidget::itemDoubleClicked, + [this](QTreeWidgetItem* item, int column) { OnSetOptionValue(item); }); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionsDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionsDialog::reject); + connect(ui->specificRadioButton, &QRadioButton::toggled, this, &OptionsDialog::PopulateOptions); +} + +OptionsDialog::~OptionsDialog() = default; diff --git a/src/citra_qt/dumping/options_dialog.h b/src/citra_qt/dumping/options_dialog.h new file mode 100644 index 000000000..d9a29e2dc --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.h @@ -0,0 +1,36 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/param_package.h" +#include "core/dumping/ffmpeg_backend.h" + +class QTreeWidgetItem; + +namespace Ui { +class OptionsDialog; +} + +class OptionsDialog : public QDialog { + Q_OBJECT + +public: + explicit OptionsDialog(QWidget* parent, std::vector specific_options, + std::vector generic_options, + const std::string& current_value); + ~OptionsDialog() override; + + std::string GetCurrentValue() const; + +private: + void PopulateOptions(); + void OnSetOptionValue(QTreeWidgetItem* item); + + std::unique_ptr ui; + std::vector specific_options; + std::vector generic_options; + Common::ParamPackage current_values; +}; diff --git a/src/citra_qt/dumping/options_dialog.ui b/src/citra_qt/dumping/options_dialog.ui new file mode 100644 index 000000000..e8bd7fb41 --- /dev/null +++ b/src/citra_qt/dumping/options_dialog.ui @@ -0,0 +1,71 @@ + + + OptionsDialog + + + + 0 + 0 + 650 + 350 + + + + Options + + + + + + true + + + Double click to see the description and change the values of the options. + + + + + + + + + Specific + + + true + + + + + + + Generic + + + + + + + + + + Name + + + + + Value + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index bd1c95c52..f6dc3f2ef 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -89,6 +89,10 @@ #include "citra_qt/discord_impl.h" #endif +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER +#include "citra_qt/dumping/dumping_dialog.h" +#endif + #ifdef QT_STATICPLUGIN Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); #endif @@ -100,6 +104,8 @@ __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; } #endif +constexpr int default_mouse_timeout = 2500; + /** * "Callouts" are one-time instructional messages shown to the user. In the config settings, there * is a bitfield "callout_flags" options, used to track if a message has already been shown to the @@ -193,6 +199,14 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { // Show one-time "callout" messages to the user ShowTelemetryCallout(); + // make sure menubar has the arrow cursor instead of inheriting from this + ui.menubar->setCursor(QCursor()); + statusBar()->setCursor(QCursor()); + + mouse_hide_timer.setInterval(default_mouse_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); + connect(ui.menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor); + if (UISettings::values.check_for_update_on_start) { CheckForUpdates(); } @@ -713,9 +727,7 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_Capture_Screenshot, &QAction::triggered, this, &GMainWindow::OnCaptureScreenshot); -#ifndef ENABLE_FFMPEG_VIDEO_DUMPER - ui.action_Dump_Video->setEnabled(false); -#endif +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER connect(ui.action_Dump_Video, &QAction::triggered, [this] { if (ui.action_Dump_Video->isChecked()) { OnStartVideoDumping(); @@ -723,6 +735,9 @@ void GMainWindow::ConnectMenuEvents() { OnStopVideoDumping(); } }); +#else + ui.action_Dump_Video->setEnabled(false); +#endif // Help connect(ui.action_Open_Citra_Folder, &QAction::triggered, this, @@ -994,6 +1009,13 @@ void GMainWindow::BootGame(const QString& filename) { } status_bar_update_timer.start(2000); + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + setMouseTracking(true); + ui.centralwidget->setMouseTracking(true); + ui.menubar->setMouseTracking(true); + } + // show and hide the render_window to create the context render_window->show(); render_window->hide(); @@ -1009,8 +1031,14 @@ void GMainWindow::BootGame(const QString& filename) { if (video_dumping_on_start) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; - Core::System::GetInstance().VideoDumper().StartDumping(video_dumping_path.toStdString(), - "webm", layout); + if (!Core::System::GetInstance().VideoDumper().StartDumping( + video_dumping_path.toStdString(), layout)) { + + QMessageBox::critical( + this, tr("Citra"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui.action_Dump_Video->setChecked(false); + } video_dumping_on_start = false; video_dumping_path.clear(); } @@ -1026,11 +1054,13 @@ void GMainWindow::ShutdownGame() { HideFullscreen(); } +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER if (Core::System::GetInstance().VideoDumper().IsDumping()) { game_shutdown_delayed = true; OnStopVideoDumping(); return; } +#endif AllowOSSleep(); @@ -1084,6 +1114,10 @@ void GMainWindow::ShutdownGame() { game_list->show(); game_list->setFilterFocus(); + setMouseTracking(false); + ui.centralwidget->setMouseTracking(false); + ui.menubar->setMouseTracking(false); + // Disable status bar updates status_bar_update_timer.stop(); message_label->setVisible(false); @@ -1290,7 +1324,7 @@ void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { using FutureWatcher = QFutureWatcher>; auto* future_watcher = new FutureWatcher(this); connect(future_watcher, &FutureWatcher::finished, - [this, program_id, dialog, base_path, update_path, future_watcher] { + [this, dialog, base_path, update_path, future_watcher] { dialog->hide(); const auto& [base, update] = future_watcher->result(); if (base != Loader::ResultStatus::Success) { @@ -1676,6 +1710,16 @@ void GMainWindow::OnConfigure() { SyncMenuUISettings(); game_list->RefreshGameDirectory(); config->Save(); + if (UISettings::values.hide_mouse && emulation_running) { + setMouseTracking(true); + ui.centralwidget->setMouseTracking(true); + ui.menubar->setMouseTracking(true); + mouse_hide_timer.start(); + } else { + setMouseTracking(false); + ui.centralwidget->setMouseTracking(false); + ui.menubar->setMouseTracking(false); + } } else { Settings::values.input_profiles = old_input_profiles; Settings::LoadProfile(old_input_profile_index); @@ -1915,18 +1959,23 @@ void GMainWindow::OnCaptureScreenshot() { OnStartGame(); } +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER void GMainWindow::OnStartVideoDumping() { - const QString path = QFileDialog::getSaveFileName( - this, tr("Save Video"), UISettings::values.video_dumping_path, tr("WebM Videos (*.webm)")); - if (path.isEmpty()) { + DumpingDialog dialog(this); + if (dialog.exec() != QDialog::DialogCode::Accepted) { ui.action_Dump_Video->setChecked(false); return; } - UISettings::values.video_dumping_path = QFileInfo(path).path(); + const auto path = dialog.GetFilePath(); if (emulation_running) { Layout::FramebufferLayout layout{ Layout::FrameLayoutFromResolutionScale(VideoCore::GetResolutionScaleFactor())}; - Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), "webm", layout); + if (!Core::System::GetInstance().VideoDumper().StartDumping(path.toStdString(), layout)) { + QMessageBox::critical( + this, tr("Citra"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui.action_Dump_Video->setChecked(false); + } } else { video_dumping_on_start = true; video_dumping_path = path; @@ -1943,6 +1992,8 @@ void GMainWindow::OnStopVideoDumping() { const bool was_dumping = Core::System::GetInstance().VideoDumper().IsDumping(); if (!was_dumping) return; + + game_paused_for_dumping = emu_thread->IsRunning(); OnPauseGame(); auto future = @@ -1952,13 +2003,15 @@ void GMainWindow::OnStopVideoDumping() { if (game_shutdown_delayed) { game_shutdown_delayed = false; ShutdownGame(); - } else { + } else if (game_paused_for_dumping) { + game_paused_for_dumping = false; OnStartGame(); } }); future_watcher->setFuture(future); } } +#endif void GMainWindow::UpdateStatusBar() { if (emu_thread == nullptr) { @@ -1983,6 +2036,30 @@ void GMainWindow::UpdateStatusBar() { emu_frametime_label->setVisible(true); } +void GMainWindow::HideMouseCursor() { + if (emu_thread == nullptr || UISettings::values.hide_mouse == false) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + setCursor(QCursor(Qt::BlankCursor)); +} + +void GMainWindow::ShowMouseCursor() { + unsetCursor(); + if (emu_thread != nullptr && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void GMainWindow::mouseMoveEvent(QMouseEvent* event) { + ShowMouseCursor(); +} + +void GMainWindow::mousePressEvent(QMouseEvent* event) { + ShowMouseCursor(); +} + void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { QString status_message; diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 63979c6b5..234b9893a 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -207,8 +207,10 @@ private slots: void OnPlayMovie(); void OnStopRecordingPlayback(); void OnCaptureScreenshot(); +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER void OnStartVideoDumping(); void OnStopVideoDumping(); +#endif void OnCoreError(Core::System::ResultStatus, std::string); /// Called whenever a user selects Help->About Citra void OnMenuAboutCitra(); @@ -225,6 +227,8 @@ private: void UpdateWindowTitle(); void RetranslateStatusBar(); void InstallCIA(QStringList filepaths); + void HideMouseCursor(); + void ShowMouseCursor(); Ui::MainWindow ui; @@ -253,6 +257,7 @@ private: QString game_path; bool auto_paused = false; + QTimer mouse_hide_timer; // Movie bool movie_record_on_start = false; @@ -263,6 +268,8 @@ private: QString video_dumping_path; // Whether game shutdown is delayed due to video dumping bool game_shutdown_delayed = false; + // Whether game was paused due to stopping video dumping + bool game_paused_for_dumping = false; // Debugger panes ProfilerWidget* profilerWidget; @@ -301,6 +308,8 @@ protected: void dropEvent(QDropEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; }; Q_DECLARE_METATYPE(std::size_t); diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h index 0736db6ff..a54bc3ee7 100644 --- a/src/citra_qt/uisettings.h +++ b/src/citra_qt/uisettings.h @@ -76,6 +76,7 @@ struct Values { bool confirm_before_closing; bool first_start; bool pause_when_in_background; + bool hide_mouse; bool updater_found; bool update_on_close; diff --git a/src/common/param_package.cpp b/src/common/param_package.cpp index 433b34b36..3a218efbc 100644 --- a/src/common/param_package.cpp +++ b/src/common/param_package.cpp @@ -135,4 +135,20 @@ void ParamPackage::Clear() { data.clear(); } +ParamPackage::DataType::iterator ParamPackage::begin() { + return data.begin(); +} + +ParamPackage::DataType::const_iterator ParamPackage::begin() const { + return data.begin(); +} + +ParamPackage::DataType::iterator ParamPackage::end() { + return data.end(); +} + +ParamPackage::DataType::const_iterator ParamPackage::end() const { + return data.end(); +} + } // namespace Common diff --git a/src/common/param_package.h b/src/common/param_package.h index 6a0a9b656..1fffb5035 100644 --- a/src/common/param_package.h +++ b/src/common/param_package.h @@ -5,15 +5,15 @@ #pragma once #include +#include #include -#include namespace Common { /// A string-based key-value container supporting serializing to and deserializing from a string class ParamPackage { public: - using DataType = std::unordered_map; + using DataType = std::map; ParamPackage() = default; explicit ParamPackage(const std::string& serialized); @@ -35,6 +35,12 @@ public: void Erase(const std::string& key); void Clear(); + // For range-based for + DataType::iterator begin(); + DataType::const_iterator begin() const; + DataType::iterator end(); + DataType::const_iterator end() const; + private: DataType data; }; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9a1e3f783..5681d4715 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -495,5 +495,5 @@ if (ARCHITECTURE_x86_64) endif() if (ENABLE_FFMPEG_VIDEO_DUMPER) - target_link_libraries(core PRIVATE FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil) + target_link_libraries(core PUBLIC FFmpeg::avcodec FFmpeg::avformat FFmpeg::swscale FFmpeg::swresample FFmpeg::avutil) endif() diff --git a/src/core/core.cpp b/src/core/core.cpp index 58f1ddefe..95a2a5ef8 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -377,6 +377,12 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo Service::Init(*this); GDBStub::DeferStart(); +#ifdef ENABLE_FFMPEG_VIDEO_DUMPER + video_dumper = std::make_unique(); +#else + video_dumper = std::make_unique(); +#endif + VideoCore::ResultStatus result = VideoCore::Init(emu_window, *memory); if (result != VideoCore::ResultStatus::Success) { switch (result) { @@ -389,12 +395,6 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo } } -#ifdef ENABLE_FFMPEG_VIDEO_DUMPER - video_dumper = std::make_unique(); -#else - video_dumper = std::make_unique(); -#endif - LOG_DEBUG(Core, "Initialized OK"); initalized = true; diff --git a/src/core/dumping/backend.cpp b/src/core/dumping/backend.cpp index daf43c744..88686b7a2 100644 --- a/src/core/dumping/backend.cpp +++ b/src/core/dumping/backend.cpp @@ -8,17 +8,7 @@ namespace VideoDumper { VideoFrame::VideoFrame(std::size_t width_, std::size_t height_, u8* data_) - : width(width_), height(height_), stride(width * 4), data(width * height * 4) { - // While copying, rotate the image to put the pixels in correct order - // (As OpenGL returns pixel data starting from the lowest position) - for (std::size_t i = 0; i < height; i++) { - for (std::size_t j = 0; j < width; j++) { - for (std::size_t k = 0; k < 4; k++) { - data[i * stride + j * 4 + k] = data_[(height - i - 1) * stride + j * 4 + k]; - } - } - } -} + : width(width_), height(height_), stride(width * 4), data(data_, data_ + width * height * 4) {} Backend::~Backend() = default; NullBackend::~NullBackend() = default; diff --git a/src/core/dumping/backend.h b/src/core/dumping/backend.h index c2a4d532a..b0b63ba66 100644 --- a/src/core/dumping/backend.h +++ b/src/core/dumping/backend.h @@ -28,10 +28,9 @@ public: class Backend { public: virtual ~Backend(); - virtual bool StartDumping(const std::string& path, const std::string& format, - const Layout::FramebufferLayout& layout) = 0; - virtual void AddVideoFrame(const VideoFrame& frame) = 0; - virtual void AddAudioFrame(const AudioCore::StereoFrame16& frame) = 0; + virtual bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) = 0; + virtual void AddVideoFrame(VideoFrame frame) = 0; + virtual void AddAudioFrame(AudioCore::StereoFrame16 frame) = 0; virtual void AddAudioSample(const std::array& sample) = 0; virtual void StopDumping() = 0; virtual bool IsDumping() const = 0; @@ -41,12 +40,12 @@ public: class NullBackend : public Backend { public: ~NullBackend() override; - bool StartDumping(const std::string& /*path*/, const std::string& /*format*/, + bool StartDumping(const std::string& /*path*/, const Layout::FramebufferLayout& /*layout*/) override { return false; } - void AddVideoFrame(const VideoFrame& /*frame*/) override {} - void AddAudioFrame(const AudioCore::StereoFrame16& /*frame*/) override {} + void AddVideoFrame(VideoFrame /*frame*/) override {} + void AddAudioFrame(AudioCore::StereoFrame16 /*frame*/) override {} void AddAudioSample(const std::array& /*sample*/) override {} void StopDumping() override {} bool IsDumping() const override { diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp index 811a5c99b..05cc25acf 100644 --- a/src/core/dumping/ffmpeg_backend.cpp +++ b/src/core/dumping/ffmpeg_backend.cpp @@ -2,15 +2,19 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include "common/assert.h" #include "common/file_util.h" #include "common/logging/log.h" +#include "common/param_package.h" +#include "common/string_util.h" #include "core/dumping/ffmpeg_backend.h" +#include "core/settings.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" extern "C" { -#include +#include } namespace VideoDumper { @@ -27,14 +31,25 @@ void InitializeFFmpegLibraries() { initialized = true; } +AVDictionary* ToAVDictionary(const std::string& serialized) { + Common::ParamPackage param_package{serialized}; + AVDictionary* result = nullptr; + for (const auto& [key, value] : param_package) { + av_dict_set(&result, key.c_str(), value.c_str(), 0); + } + return result; +} + FFmpegStream::~FFmpegStream() { Free(); } -bool FFmpegStream::Init(AVFormatContext* format_context_) { +bool FFmpegStream::Init(FFmpegMuxer& muxer) { InitializeFFmpegLibraries(); - format_context = format_context_; + format_context = muxer.format_context.get(); + format_context_mutex = &muxer.format_context_mutex; + return true; } @@ -47,14 +62,12 @@ void FFmpegStream::Flush() { } void FFmpegStream::WritePacket(AVPacket& packet) { - if (packet.pts != static_cast(AV_NOPTS_VALUE)) { - packet.pts = av_rescale_q(packet.pts, codec_context->time_base, stream->time_base); - } - if (packet.dts != static_cast(AV_NOPTS_VALUE)) { - packet.dts = av_rescale_q(packet.dts, codec_context->time_base, stream->time_base); - } + av_packet_rescale_ts(&packet, codec_context->time_base, stream->time_base); packet.stream_index = stream->index; - av_interleaved_write_frame(format_context, &packet); + { + std::lock_guard lock{*format_context_mutex}; + av_interleaved_write_frame(format_context, &packet); + } } void FFmpegStream::SendFrame(AVFrame* frame) { @@ -88,21 +101,18 @@ FFmpegVideoStream::~FFmpegVideoStream() { Free(); } -bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* output_format, - const Layout::FramebufferLayout& layout_) { +bool FFmpegVideoStream::Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout& layout_) { InitializeFFmpegLibraries(); - if (!FFmpegStream::Init(format_context)) + if (!FFmpegStream::Init(muxer)) return false; layout = layout_; frame_count = 0; // Initialize video codec - // Ensure VP9 codec here, also to avoid patent issues - constexpr AVCodecID codec_id = AV_CODEC_ID_VP9; - const AVCodec* codec = avcodec_find_encoder(codec_id); + const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.video_encoder.c_str()); codec_context.reset(avcodec_alloc_context3(codec)); if (!codec || !codec_context) { LOG_ERROR(Render, "Could not find video encoder or allocate video codec context"); @@ -111,23 +121,28 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou // Configure video codec context codec_context->codec_type = AVMEDIA_TYPE_VIDEO; - codec_context->bit_rate = 2500000; + codec_context->bit_rate = Settings::values.video_bitrate; codec_context->width = layout.width; codec_context->height = layout.height; codec_context->time_base.num = 1; codec_context->time_base.den = 60; codec_context->gop_size = 12; - codec_context->pix_fmt = AV_PIX_FMT_YUV420P; - codec_context->thread_count = 8; - if (output_format->flags & AVFMT_GLOBALHEADER) + codec_context->pix_fmt = codec->pix_fmts ? codec->pix_fmts[0] : AV_PIX_FMT_YUV420P; + if (format_context->oformat->flags & AVFMT_GLOBALHEADER) codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; - av_opt_set_int(codec_context.get(), "cpu-used", 5, 0); - if (avcodec_open2(codec_context.get(), codec, nullptr) < 0) { + AVDictionary* options = ToAVDictionary(Settings::values.video_encoder_options); + if (avcodec_open2(codec_context.get(), codec, &options) < 0) { LOG_ERROR(Render, "Could not open video codec"); return false; } + if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict + char* buf = nullptr; + av_dict_get_string(options, &buf, ':', ';'); + LOG_WARNING(Render, "Video encoder options not found: {}", buf); + } + // Create video stream stream = avformat_new_stream(format_context, codec); if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) { @@ -141,7 +156,7 @@ bool FFmpegVideoStream::Init(AVFormatContext* format_context, AVOutputFormat* ou scaled_frame->format = codec_context->pix_fmt; scaled_frame->width = layout.width; scaled_frame->height = layout.height; - if (av_frame_get_buffer(scaled_frame.get(), 1) < 0) { + if (av_frame_get_buffer(scaled_frame.get(), 0) < 0) { LOG_ERROR(Render, "Could not allocate frame buffer"); return false; } @@ -177,6 +192,10 @@ void FFmpegVideoStream::ProcessFrame(VideoFrame& frame) { current_frame->height = layout.height; // Scale the frame + if (av_frame_make_writable(scaled_frame.get()) < 0) { + LOG_ERROR(Render, "Video frame dropped: Could not prepare frame"); + return; + } if (sws_context) { sws_scale(sws_context.get(), current_frame->data, current_frame->linesize, 0, layout.height, scaled_frame->data, scaled_frame->linesize); @@ -191,17 +210,16 @@ FFmpegAudioStream::~FFmpegAudioStream() { Free(); } -bool FFmpegAudioStream::Init(AVFormatContext* format_context) { +bool FFmpegAudioStream::Init(FFmpegMuxer& muxer) { InitializeFFmpegLibraries(); - if (!FFmpegStream::Init(format_context)) + if (!FFmpegStream::Init(muxer)) return false; - sample_count = 0; + frame_count = 0; // Initialize audio codec - constexpr AVCodecID codec_id = AV_CODEC_ID_VORBIS; - const AVCodec* codec = avcodec_find_encoder(codec_id); + const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.audio_encoder.c_str()); codec_context.reset(avcodec_alloc_context3(codec)); if (!codec || !codec_context) { LOG_ERROR(Render, "Could not find audio encoder or allocate audio codec context"); @@ -210,17 +228,52 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { // Configure audio codec context codec_context->codec_type = AVMEDIA_TYPE_AUDIO; - codec_context->bit_rate = 64000; - codec_context->sample_fmt = codec->sample_fmts[0]; - codec_context->sample_rate = AudioCore::native_sample_rate; + codec_context->bit_rate = Settings::values.audio_bitrate; + if (codec->sample_fmts) { + codec_context->sample_fmt = codec->sample_fmts[0]; + } else { + codec_context->sample_fmt = AV_SAMPLE_FMT_S16P; + } + + if (codec->supported_samplerates) { + codec_context->sample_rate = codec->supported_samplerates[0]; + // Prefer native sample rate if supported + const int* ptr = codec->supported_samplerates; + while ((*ptr)) { + if ((*ptr) == AudioCore::native_sample_rate) { + codec_context->sample_rate = AudioCore::native_sample_rate; + break; + } + ptr++; + } + } else { + codec_context->sample_rate = AudioCore::native_sample_rate; + } + codec_context->time_base.num = 1; + codec_context->time_base.den = codec_context->sample_rate; codec_context->channel_layout = AV_CH_LAYOUT_STEREO; codec_context->channels = 2; + if (format_context->oformat->flags & AVFMT_GLOBALHEADER) + codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; - if (avcodec_open2(codec_context.get(), codec, nullptr) < 0) { + AVDictionary* options = ToAVDictionary(Settings::values.audio_encoder_options); + if (avcodec_open2(codec_context.get(), codec, &options) < 0) { LOG_ERROR(Render, "Could not open audio codec"); return false; } + if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict + char* buf = nullptr; + av_dict_get_string(options, &buf, ':', ';'); + LOG_WARNING(Render, "Audio encoder options not found: {}", buf); + } + + if (codec_context->frame_size) { + frame_size = static_cast(codec_context->frame_size); + } else { // variable frame size support + frame_size = std::tuple_size::value; + } + // Create audio stream stream = avformat_new_stream(format_context, codec); if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) { @@ -234,6 +287,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { audio_frame->format = codec_context->sample_fmt; audio_frame->channel_layout = codec_context->channel_layout; audio_frame->channels = codec_context->channels; + audio_frame->sample_rate = codec_context->sample_rate; // Allocate SWR context auto* context = @@ -253,7 +307,7 @@ bool FFmpegAudioStream::Init(AVFormatContext* format_context) { // Allocate resampled data int error = av_samples_alloc_array_and_samples(&resampled_data, nullptr, codec_context->channels, - codec_context->frame_size, codec_context->sample_fmt, 0); + frame_size, codec_context->sample_fmt, 0); if (error < 0) { LOG_ERROR(Render, "Could not allocate samples storage"); return false; @@ -274,39 +328,79 @@ void FFmpegAudioStream::Free() { av_freep(&resampled_data); } -void FFmpegAudioStream::ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) { +void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0, + const VariableAudioFrame& channel1) { ASSERT_MSG(channel0.size() == channel1.size(), "Frames of the two channels must have the same number of samples"); - std::array src_data = {reinterpret_cast(channel0.data()), - reinterpret_cast(channel1.data())}; - if (swr_convert(swr_context.get(), resampled_data, channel0.size(), src_data.data(), - channel0.size()) < 0) { + const auto sample_size = av_get_bytes_per_sample(codec_context->sample_fmt); + std::array src_data = {reinterpret_cast(channel0.data()), + reinterpret_cast(channel1.data())}; + + std::array dst_data; + if (av_sample_fmt_is_planar(codec_context->sample_fmt)) { + dst_data = {resampled_data[0] + sample_size * offset, + resampled_data[1] + sample_size * offset}; + } else { + dst_data = {resampled_data[0] + sample_size * offset * 2}; // 2 channels + } + + auto resampled_count = swr_convert(swr_context.get(), dst_data.data(), frame_size - offset, + src_data.data(), channel0.size()); + if (resampled_count < 0) { LOG_ERROR(Render, "Audio frame dropped: Could not resample data"); return; } - // Prepare frame - audio_frame->nb_samples = channel0.size(); - audio_frame->data[0] = resampled_data[0]; - audio_frame->data[1] = resampled_data[1]; - audio_frame->pts = sample_count; - sample_count += channel0.size(); + offset += resampled_count; + if (offset < frame_size) { // Still not enough to form a frame + return; + } - SendFrame(audio_frame.get()); + while (true) { + // Prepare frame + audio_frame->nb_samples = frame_size; + audio_frame->data[0] = resampled_data[0]; + if (av_sample_fmt_is_planar(codec_context->sample_fmt)) { + audio_frame->data[1] = resampled_data[1]; + } + audio_frame->pts = frame_count * frame_size; + frame_count++; + + SendFrame(audio_frame.get()); + + // swr_convert buffers input internally. Try to get more resampled data + resampled_count = swr_convert(swr_context.get(), resampled_data, frame_size, nullptr, 0); + if (resampled_count < 0) { + LOG_ERROR(Render, "Audio frame dropped: Could not resample data"); + return; + } + if (static_cast(resampled_count) < frame_size) { + offset = resampled_count; + break; + } + } } -std::size_t FFmpegAudioStream::GetAudioFrameSize() const { - ASSERT_MSG(codec_context, "Codec context is not initialized yet!"); - return codec_context->frame_size; +void FFmpegAudioStream::Flush() { + // Send the last samples + audio_frame->nb_samples = offset; + audio_frame->data[0] = resampled_data[0]; + if (av_sample_fmt_is_planar(codec_context->sample_fmt)) { + audio_frame->data[1] = resampled_data[1]; + } + audio_frame->pts = frame_count * frame_size; + + SendFrame(audio_frame.get()); + + FFmpegStream::Flush(); } FFmpegMuxer::~FFmpegMuxer() { Free(); } -bool FFmpegMuxer::Init(const std::string& path, const std::string& format, - const Layout::FramebufferLayout& layout) { +bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout& layout) { InitializeFFmpegLibraries(); @@ -315,9 +409,8 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format, } // Get output format - // Ensure webm here to avoid patent issues - ASSERT_MSG(format == "webm", "Only webm is allowed for frame dumping"); - auto* output_format = av_guess_format(format.c_str(), path.c_str(), "video/webm"); + const auto format = Settings::values.output_format; + auto* output_format = av_guess_format(format.c_str(), path.c_str(), nullptr); if (!output_format) { LOG_ERROR(Render, "Could not get format {}", format); return false; @@ -333,18 +426,24 @@ bool FFmpegMuxer::Init(const std::string& path, const std::string& format, } format_context.reset(format_context_raw); - if (!video_stream.Init(format_context.get(), output_format, layout)) + if (!video_stream.Init(*this, layout)) return false; - if (!audio_stream.Init(format_context.get())) + if (!audio_stream.Init(*this)) return false; + AVDictionary* options = ToAVDictionary(Settings::values.format_options); // Open video file if (avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 || - avformat_write_header(format_context.get(), nullptr)) { + avformat_write_header(format_context.get(), &options)) { LOG_ERROR(Render, "Could not open {}", path); return false; } + if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict + char* buf = nullptr; + av_dict_get_string(options, &buf, ':', ';'); + LOG_WARNING(Render, "Format options not found: {}", buf); + } LOG_INFO(Render, "Dumping frames to {} ({}x{})", path, layout.width, layout.height); return true; @@ -360,7 +459,8 @@ void FFmpegMuxer::ProcessVideoFrame(VideoFrame& frame) { video_stream.ProcessFrame(frame); } -void FFmpegMuxer::ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1) { +void FFmpegMuxer::ProcessAudioFrame(const VariableAudioFrame& channel0, + const VariableAudioFrame& channel1) { audio_stream.ProcessFrame(channel0, channel1); } @@ -372,11 +472,9 @@ void FFmpegMuxer::FlushAudio() { audio_stream.Flush(); } -std::size_t FFmpegMuxer::GetAudioFrameSize() const { - return audio_stream.GetAudioFrameSize(); -} - void FFmpegMuxer::WriteTrailer() { + std::lock_guard lock{format_context_mutex}; + av_interleaved_write_frame(format_context.get(), nullptr); av_write_trailer(format_context.get()); } @@ -392,12 +490,11 @@ FFmpegBackend::~FFmpegBackend() { ffmpeg.Free(); } -bool FFmpegBackend::StartDumping(const std::string& path, const std::string& format, - const Layout::FramebufferLayout& layout) { +bool FFmpegBackend::StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) { InitializeFFmpegLibraries(); - if (!ffmpeg.Init(path, format, layout)) { + if (!ffmpeg.Init(path, layout)) { ffmpeg.Free(); return false; } @@ -450,31 +547,29 @@ bool FFmpegBackend::StartDumping(const std::string& path, const std::string& for return true; } -void FFmpegBackend::AddVideoFrame(const VideoFrame& frame) { +void FFmpegBackend::AddVideoFrame(VideoFrame frame) { event1.Wait(); video_frame_buffers[next_buffer] = std::move(frame); event2.Set(); } -void FFmpegBackend::AddAudioFrame(const AudioCore::StereoFrame16& frame) { - std::array, 2> refactored_frame; +void FFmpegBackend::AddAudioFrame(AudioCore::StereoFrame16 frame) { + std::array refactored_frame; + for (auto& channel : refactored_frame) { + channel.resize(frame.size()); + } for (std::size_t i = 0; i < frame.size(); i++) { refactored_frame[0][i] = frame[i][0]; refactored_frame[1][i] = frame[i][1]; } - for (auto i : {0, 1}) { - audio_buffers[i].insert(audio_buffers[i].end(), refactored_frame[i].begin(), - refactored_frame[i].end()); - } - CheckAudioBuffer(); + audio_frame_queues[0].Push(std::move(refactored_frame[0])); + audio_frame_queues[1].Push(std::move(refactored_frame[1])); } void FFmpegBackend::AddAudioSample(const std::array& sample) { - for (auto i : {0, 1}) { - audio_buffers[i].push_back(sample[i]); - } - CheckAudioBuffer(); + audio_frame_queues[0].Push(VariableAudioFrame{sample[0]}); + audio_frame_queues[1].Push(VariableAudioFrame{sample[1]}); } void FFmpegBackend::StopDumping() { @@ -484,12 +579,6 @@ void FFmpegBackend::StopDumping() { // Flush the video processing queue AddVideoFrame(VideoFrame()); for (auto i : {0, 1}) { - // Add remaining data to audio queue - if (audio_buffers[i].size() >= 0) { - VariableAudioFrame buffer(audio_buffers[i].begin(), audio_buffers[i].end()); - audio_frame_queues[i].Push(std::move(buffer)); - audio_buffers[i].clear(); - } // Flush the audio processing queue audio_frame_queues[i].Push(VariableAudioFrame()); } @@ -513,18 +602,234 @@ void FFmpegBackend::EndDumping() { processing_ended.Set(); } -void FFmpegBackend::CheckAudioBuffer() { - for (auto i : {0, 1}) { - const std::size_t frame_size = ffmpeg.GetAudioFrameSize(); - // Add audio data to the queue when there is enough to form a frame - while (audio_buffers[i].size() >= frame_size) { - VariableAudioFrame buffer(audio_buffers[i].begin(), - audio_buffers[i].begin() + frame_size); - audio_frame_queues[i].Push(std::move(buffer)); +// To std string, but handles nullptr +std::string ToStdString(const char* str, const std::string& fallback = "") { + return str ? std::string{str} : fallback; +} - audio_buffers[i].erase(audio_buffers[i].begin(), audio_buffers[i].begin() + frame_size); +std::string FormatDuration(s64 duration) { + // The following is implemented according to libavutil code (opt.c) + std::string out; + if (duration < 0 && duration != std::numeric_limits::min()) { + out.append("-"); + duration = -duration; + } + if (duration == std::numeric_limits::max()) { + return "INT64_MAX"; + } else if (duration == std::numeric_limits::min()) { + return "INT64_MIN"; + } else if (duration > 3600ll * 1000000ll) { + out.append(fmt::format("{}:{:02d}:{:02d}.{:06d}", duration / 3600000000ll, + ((duration / 60000000ll) % 60), ((duration / 1000000ll) % 60), + duration % 1000000)); + } else if (duration > 60ll * 1000000ll) { + out.append(fmt::format("{}:{:02d}.{:06d}", duration / 60000000ll, + ((duration / 1000000ll) % 60), duration % 1000000)); + } else { + out.append(fmt::format("{}.{:06d}", duration / 1000000ll, duration % 1000000)); + } + while (out.back() == '0') { + out.erase(out.size() - 1, 1); + } + if (out.back() == '.') { + out.erase(out.size() - 1, 1); + } + return out; +} + +std::string FormatDefaultValue(const AVOption* option, + const std::vector& named_constants) { + // The following is taken and modified from libavutil code (opt.c) + switch (option->type) { + case AV_OPT_TYPE_BOOL: { + const auto value = option->default_val.i64; + if (value < 0) { + return "auto"; } + return value ? "true" : "false"; + } + case AV_OPT_TYPE_FLAGS: { + const auto value = option->default_val.i64; + std::string out; + for (const auto& constant : named_constants) { + if (!(value & constant.value)) { + continue; + } + if (!out.empty()) { + out.append("+"); + } + out.append(constant.name); + } + return out.empty() ? fmt::format("{}", value) : out; + } + case AV_OPT_TYPE_DURATION: { + return FormatDuration(option->default_val.i64); + } + case AV_OPT_TYPE_INT: + case AV_OPT_TYPE_UINT64: + case AV_OPT_TYPE_INT64: { + const auto value = option->default_val.i64; + for (const auto& constant : named_constants) { + if (constant.value == value) { + return constant.name; + } + } + return fmt::format("{}", value); + } + case AV_OPT_TYPE_DOUBLE: + case AV_OPT_TYPE_FLOAT: { + return fmt::format("{}", option->default_val.dbl); + } + case AV_OPT_TYPE_RATIONAL: { + const auto q = av_d2q(option->default_val.dbl, std::numeric_limits::max()); + return fmt::format("{}/{}", q.num, q.den); + } + case AV_OPT_TYPE_PIXEL_FMT: { + const char* name = av_get_pix_fmt_name(static_cast(option->default_val.i64)); + return ToStdString(name, "none"); + } + case AV_OPT_TYPE_SAMPLE_FMT: { + const char* name = + av_get_sample_fmt_name(static_cast(option->default_val.i64)); + return ToStdString(name, "none"); + } + case AV_OPT_TYPE_COLOR: + case AV_OPT_TYPE_IMAGE_SIZE: + case AV_OPT_TYPE_STRING: + case AV_OPT_TYPE_DICT: + case AV_OPT_TYPE_VIDEO_RATE: { + return ToStdString(option->default_val.str); + } + case AV_OPT_TYPE_CHANNEL_LAYOUT: { + return fmt::format("{:#x}", option->default_val.i64); + } + default: + return ""; } } +void GetOptionListSingle(std::vector& out, const AVClass* av_class) { + if (av_class == nullptr) { + return; + } + + const AVOption* current = nullptr; + std::unordered_map> named_constants_map; + // First iteration: find and place all named constants + while ((current = av_opt_next(&av_class, current))) { + if (current->type != AV_OPT_TYPE_CONST || !current->unit) { + continue; + } + named_constants_map[current->unit].push_back( + {current->name, ToStdString(current->help), current->default_val.i64}); + } + // Second iteration: find all options + current = nullptr; + while ((current = av_opt_next(&av_class, current))) { + // Currently we cannot handle binary options + if (current->type == AV_OPT_TYPE_CONST || current->type == AV_OPT_TYPE_BINARY) { + continue; + } + std::vector named_constants; + if (current->unit && named_constants_map.count(current->unit)) { + named_constants = named_constants_map.at(current->unit); + } + const auto default_value = FormatDefaultValue(current, named_constants); + out.push_back({current->name, ToStdString(current->help), current->type, default_value, + std::move(named_constants), current->min, current->max}); + } +} + +void GetOptionList(std::vector& out, const AVClass* av_class, bool search_children) { + if (av_class == nullptr) { + return; + } + + GetOptionListSingle(out, av_class); + + if (!search_children) { + return; + } + + const AVClass* child_class = nullptr; + while ((child_class = av_opt_child_class_next(av_class, child_class))) { + GetOptionListSingle(out, child_class); + } +} + +std::vector GetOptionList(const AVClass* av_class, bool search_children) { + std::vector out; + GetOptionList(out, av_class, search_children); + return out; +} + +std::vector ListEncoders(AVMediaType type) { + InitializeFFmpegLibraries(); + + std::vector out; + + const AVCodec* current = nullptr; +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100) + while ((current = av_codec_next(current))) { +#else + void* data = nullptr; // For libavcodec to save the iteration state + while ((current = av_codec_iterate(&data))) { +#endif + if (!av_codec_is_encoder(current) || current->type != type) { + continue; + } + out.push_back({current->name, ToStdString(current->long_name), current->id, + GetOptionList(current->priv_class, true)}); + } + return out; +} + +std::vector GetEncoderGenericOptions() { + return GetOptionList(avcodec_get_class(), false); +} + +std::vector ListFormats() { + InitializeFFmpegLibraries(); + + std::vector out; + + const AVOutputFormat* current = nullptr; +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100) + while ((current = av_oformat_next(current))) { +#else + void* data = nullptr; // For libavformat to save the iteration state + while ((current = av_muxer_iterate(&data))) { +#endif + std::vector extensions; + Common::SplitString(ToStdString(current->extensions), ',', extensions); + + std::set supported_video_codecs; + std::set supported_audio_codecs; + // Go through all codecs + const AVCodecDescriptor* codec = nullptr; + while ((codec = avcodec_descriptor_next(codec))) { + if (avformat_query_codec(current, codec->id, FF_COMPLIANCE_NORMAL) == 1) { + if (codec->type == AVMEDIA_TYPE_VIDEO) { + supported_video_codecs.emplace(codec->id); + } else if (codec->type == AVMEDIA_TYPE_AUDIO) { + supported_audio_codecs.emplace(codec->id); + } + } + } + + if (supported_video_codecs.empty() || supported_audio_codecs.empty()) { + continue; + } + + out.push_back({current->name, ToStdString(current->long_name), std::move(extensions), + std::move(supported_video_codecs), std::move(supported_audio_codecs), + GetOptionList(current->priv_class, true)}); + } + return out; +} + +std::vector GetFormatGenericOptions() { + return GetOptionList(avformat_get_class(), false); +} + } // namespace VideoDumper diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h index 0208195d5..86a6e08cc 100644 --- a/src/core/dumping/ffmpeg_backend.h +++ b/src/core/dumping/ffmpeg_backend.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include "common/common_types.h" @@ -19,6 +20,7 @@ extern "C" { #include #include +#include #include #include } @@ -29,13 +31,15 @@ using VariableAudioFrame = std::vector; void InitFFmpegLibraries(); +class FFmpegMuxer; + /** * Wrapper around FFmpeg AVCodecContext + AVStream. * Rescales/Resamples, encodes and writes a frame. */ class FFmpegStream { public: - bool Init(AVFormatContext* format_context); + bool Init(FFmpegMuxer& muxer); void Free(); void Flush(); @@ -58,6 +62,7 @@ protected: }; AVFormatContext* format_context{}; + std::mutex* format_context_mutex{}; std::unique_ptr codec_context{}; AVStream* stream{}; }; @@ -70,8 +75,7 @@ class FFmpegVideoStream : public FFmpegStream { public: ~FFmpegVideoStream(); - bool Init(AVFormatContext* format_context, AVOutputFormat* output_format, - const Layout::FramebufferLayout& layout); + bool Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout& layout); void Free(); void ProcessFrame(VideoFrame& frame); @@ -96,15 +100,16 @@ private: /** * A FFmpegStream used for audio data. * Resamples (converts), encodes and writes a frame. + * This also temporarily stores resampled audio data before there are enough to form a frame. */ class FFmpegAudioStream : public FFmpegStream { public: ~FFmpegAudioStream(); - bool Init(AVFormatContext* format_context); + bool Init(FFmpegMuxer& muxer); void Free(); - void ProcessFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1); - std::size_t GetAudioFrameSize() const; + void ProcessFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1); + void Flush(); private: struct SwrContextDeleter { @@ -113,12 +118,14 @@ private: } }; - u64 sample_count{}; + u64 frame_size{}; + u64 frame_count{}; std::unique_ptr audio_frame{}; std::unique_ptr swr_context{}; u8** resampled_data{}; + u64 offset{}; // Number of output samples that are currently in resampled_data. }; /** @@ -129,14 +136,12 @@ class FFmpegMuxer { public: ~FFmpegMuxer(); - bool Init(const std::string& path, const std::string& format, - const Layout::FramebufferLayout& layout); + bool Init(const std::string& path, const Layout::FramebufferLayout& layout); void Free(); void ProcessVideoFrame(VideoFrame& frame); - void ProcessAudioFrame(VariableAudioFrame& channel0, VariableAudioFrame& channel1); + void ProcessAudioFrame(const VariableAudioFrame& channel0, const VariableAudioFrame& channel1); void FlushVideo(); void FlushAudio(); - std::size_t GetAudioFrameSize() const; void WriteTrailer(); private: @@ -150,28 +155,28 @@ private: FFmpegAudioStream audio_stream{}; FFmpegVideoStream video_stream{}; std::unique_ptr format_context{}; + std::mutex format_context_mutex; + + friend class FFmpegStream; }; /** * FFmpeg video dumping backend. - * This class implements a double buffer, and an audio queue to keep audio data - * before enough data is received to form a frame. + * This class implements a double buffer. */ class FFmpegBackend : public Backend { public: FFmpegBackend(); ~FFmpegBackend() override; - bool StartDumping(const std::string& path, const std::string& format, - const Layout::FramebufferLayout& layout) override; - void AddVideoFrame(const VideoFrame& frame) override; - void AddAudioFrame(const AudioCore::StereoFrame16& frame) override; + bool StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) override; + void AddVideoFrame(VideoFrame frame) override; + void AddAudioFrame(AudioCore::StereoFrame16 frame) override; void AddAudioSample(const std::array& sample) override; void StopDumping() override; bool IsDumping() const override; Layout::FramebufferLayout GetLayout() const override; private: - void CheckAudioBuffer(); void EndDumping(); std::atomic_bool is_dumping = false; ///< Whether the backend is currently dumping @@ -184,13 +189,51 @@ private: Common::Event event1, event2; std::thread video_processing_thread; - /// An audio buffer used to temporarily hold audio data, before the size is big enough - /// to be sent to the encoder as a frame - std::array audio_buffers; std::array, 2> audio_frame_queues; std::thread audio_processing_thread; Common::Event processing_ended; }; +/// Struct describing encoder/muxer options +struct OptionInfo { + std::string name; + std::string description; + AVOptionType type; + std::string default_value; + struct NamedConstant { + std::string name; + std::string description; + s64 value; + }; + std::vector named_constants; + + // If this is a scalar type + double min; + double max; +}; + +/// Struct describing an encoder +struct EncoderInfo { + std::string name; + std::string long_name; + AVCodecID codec; + std::vector options; +}; + +/// Struct describing a format +struct FormatInfo { + std::string name; + std::string long_name; + std::vector extensions; + std::set supported_video_codecs; + std::set supported_audio_codecs; + std::vector options; +}; + +std::vector ListEncoders(AVMediaType type); +std::vector GetEncoderGenericOptions(); +std::vector ListFormats(); +std::vector GetFormatGenericOptions(); + } // namespace VideoDumper diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index cdd908b4e..68d84bab2 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -223,6 +223,7 @@ void Thread::ResumeFromWait() { case ThreadStatus::WaitArb: case ThreadStatus::WaitSleep: case ThreadStatus::WaitIPC: + case ThreadStatus::Dormant: break; case ThreadStatus::Ready: diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index 69b51480b..430d48cdd 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -120,15 +120,10 @@ void Module::Interface::GetSoftwareClosedFlag(Kernel::HLERequestContext& ctx) { void CheckNew3DS(IPC::RequestBuilder& rb) { const bool is_new_3ds = Settings::values.is_new_3ds; - if (is_new_3ds) { - LOG_CRITICAL(Service_PTM, "The option 'is_new_3ds' is enabled as part of the 'System' " - "settings. Citra does not fully support New 3DS emulation yet!"); - } - rb.Push(RESULT_SUCCESS); rb.Push(is_new_3ds); - LOG_WARNING(Service_PTM, "(STUBBED) called isNew3DS = 0x{:08x}", static_cast(is_new_3ds)); + LOG_DEBUG(Service_PTM, "called isNew3DS = 0x{:08x}", static_cast(is_new_3ds)); } void Module::Interface::CheckNew3DS(Kernel::HLERequestContext& ctx) { diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 13b1e965f..6af80205b 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -13,7 +13,6 @@ #include "core/hle/service/mic_u.h" #include "core/settings.h" #include "video_core/renderer_base.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" #include "video_core/video_core.h" namespace Settings { @@ -38,9 +37,7 @@ void Apply() { VideoCore::g_renderer_bg_color_update_requested = true; VideoCore::g_renderer_sampler_update_requested = true; VideoCore::g_renderer_shader_update_requested = true; - - OpenGL::TextureFilterManager::GetInstance().SetTextureFilter(values.texture_filter_name, - values.texture_filter_factor); + VideoCore::g_texture_filter_update_requested = true; auto& system = Core::System::GetInstance(); if (system.IsPoweredOn()) { @@ -88,7 +85,6 @@ void LogSettings() { LogSetting("Renderer_FrameLimit", Settings::values.frame_limit); LogSetting("Renderer_PostProcessingShader", Settings::values.pp_shader_name); LogSetting("Renderer_FilterMode", Settings::values.filter_mode); - LogSetting("Renderer_TextureFilterFactor", Settings::values.texture_filter_factor); LogSetting("Renderer_TextureFilterName", Settings::values.texture_filter_name); LogSetting("Stereoscopy_Render3d", static_cast(Settings::values.render_3d)); LogSetting("Stereoscopy_Factor3d", Settings::values.factor_3d); diff --git a/src/core/settings.h b/src/core/settings.h index aa7e781b5..70bdd8c4c 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -148,7 +148,6 @@ struct Values { u16 resolution_factor; bool use_frame_limit; u16 frame_limit; - u16 texture_filter_factor; std::string texture_filter_name; LayoutOption layout_option; @@ -207,6 +206,18 @@ struct Values { std::string web_api_url; std::string citra_username; std::string citra_token; + + // Video Dumping + std::string output_format; + std::string format_options; + + std::string video_encoder; + std::string video_encoder_options; + u64 video_bitrate; + + std::string audio_encoder; + std::string audio_encoder_options; + u64 audio_bitrate; } extern values; // a special value for Values::region_value indicating that citra will automatically select a region diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp index 0b69bfede..f186787b8 100644 --- a/src/input_common/sdl/sdl_impl.cpp +++ b/src/input_common/sdl/sdl_impl.cpp @@ -472,6 +472,14 @@ SDLState::SDLState() { if (SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1") == SDL_FALSE) { LOG_ERROR(Input, "Failed to set Hint for background events", SDL_GetError()); } +// these hints are only defined on sdl2.0.9 or higher +#if SDL_VERSION_ATLEAST(2, 0, 9) + // This can be set back to 1 when the compatibility problems with the controllers are + // solved. There are also hints to toggle the individual drivers. + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0"); + // This hint should probably stay as "0" as long as the hidapi PS4 led issue isn't fixed + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4, "0"); +#endif SDL_AddEventWatch(&SDLEventWatcher, this); diff --git a/src/network/room.cpp b/src/network/room.cpp index e1b6b77d3..342a2541b 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -53,7 +53,7 @@ public: mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists RoomImpl() - : random_gen(std::random_device()()), NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00} {} + : NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00}, random_gen(std::random_device()()) {} /// Thread that receives and dispatches network packets std::unique_ptr room_thread; diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp index 5814fe12a..e43004027 100644 --- a/src/network/room_member.cpp +++ b/src/network/room_member.cpp @@ -242,6 +242,13 @@ void RoomMember::RoomMemberImpl::MemberLoop() { SetError(Error::LostConnection); } break; + case ENET_EVENT_TYPE_NONE: + break; + case ENET_EVENT_TYPE_CONNECT: + // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're + // already connected + ASSERT_MSG(false, "Received unexpected connect event while already connected"); + break; } } { diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 53862bc63..eb76050fc 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -23,6 +23,8 @@ add_library(video_core STATIC regs_texturing.h renderer_base.cpp renderer_base.h + renderer_opengl/frame_dumper_opengl.cpp + renderer_opengl/frame_dumper_opengl.h renderer_opengl/gl_rasterizer.cpp renderer_opengl/gl_rasterizer.h renderer_opengl/gl_rasterizer_cache.cpp @@ -43,6 +45,8 @@ add_library(video_core STATIC renderer_opengl/gl_state.h renderer_opengl/gl_stream_buffer.cpp renderer_opengl/gl_stream_buffer.h + renderer_opengl/gl_surface_params.cpp + renderer_opengl/gl_surface_params.h renderer_opengl/gl_vars.cpp renderer_opengl/gl_vars.h renderer_opengl/pica_to_gl.h @@ -54,11 +58,14 @@ add_library(video_core STATIC renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h renderer_opengl/texture_filters/bicubic/bicubic.cpp renderer_opengl/texture_filters/bicubic/bicubic.h - renderer_opengl/texture_filters/texture_filter_interface.h - renderer_opengl/texture_filters/texture_filter_manager.cpp - renderer_opengl/texture_filters/texture_filter_manager.h + renderer_opengl/texture_filters/texture_filter_base.h + renderer_opengl/texture_filters/texture_filterer.cpp + renderer_opengl/texture_filters/texture_filterer.h renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp renderer_opengl/texture_filters/xbrz/xbrz_freescale.h + #temporary, move these back in alphabetical order before merging + renderer_opengl/gl_format_reinterpreter.cpp + renderer_opengl/gl_format_reinterpreter.h shader/debug_data.h shader/shader.cpp shader/shader.h diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.cpp b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp new file mode 100644 index 000000000..53985823c --- /dev/null +++ b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp @@ -0,0 +1,98 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "core/frontend/emu_window.h" +#include "core/frontend/scope_acquire_context.h" +#include "video_core/renderer_opengl/frame_dumper_opengl.h" +#include "video_core/renderer_opengl/renderer_opengl.h" + +namespace OpenGL { + +FrameDumperOpenGL::FrameDumperOpenGL(VideoDumper::Backend& video_dumper_, + Frontend::EmuWindow& emu_window) + : video_dumper(video_dumper_), context(emu_window.CreateSharedContext()) {} + +FrameDumperOpenGL::~FrameDumperOpenGL() { + if (present_thread.joinable()) + present_thread.join(); +} + +bool FrameDumperOpenGL::IsDumping() const { + return video_dumper.IsDumping(); +} + +Layout::FramebufferLayout FrameDumperOpenGL::GetLayout() const { + return video_dumper.GetLayout(); +} + +void FrameDumperOpenGL::StartDumping() { + if (present_thread.joinable()) + present_thread.join(); + + present_thread = std::thread(&FrameDumperOpenGL::PresentLoop, this); +} + +void FrameDumperOpenGL::StopDumping() { + stop_requested.store(true, std::memory_order_relaxed); +} + +void FrameDumperOpenGL::PresentLoop() { + Frontend::ScopeAcquireContext scope{*context}; + InitializeOpenGLObjects(); + + const auto& layout = GetLayout(); + while (!stop_requested.exchange(false)) { + auto frame = mailbox->TryGetPresentFrame(200); + if (!frame) { + continue; + } + + if (frame->color_reloaded) { + LOG_DEBUG(Render_OpenGL, "Reloading present frame"); + mailbox->ReloadPresentFrame(frame, layout.width, layout.height); + } + glWaitSync(frame->render_fence, 0, GL_TIMEOUT_IGNORED); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, frame->present.handle); + glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[current_pbo].handle); + glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0); + + // Insert fence for the main thread to block on + frame->present_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + + // Bind the previous PBO and read the pixels + glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[next_pbo].handle); + GLubyte* pixels = static_cast(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY)); + VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels}; + video_dumper.AddVideoFrame(std::move(frame_data)); + glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + + current_pbo = (current_pbo + 1) % 2; + next_pbo = (current_pbo + 1) % 2; + } + + CleanupOpenGLObjects(); +} + +void FrameDumperOpenGL::InitializeOpenGLObjects() { + const auto& layout = GetLayout(); + for (auto& buffer : pbos) { + buffer.Create(); + glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle); + glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr, + GL_STREAM_READ); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + } +} + +void FrameDumperOpenGL::CleanupOpenGLObjects() { + for (auto& buffer : pbos) { + buffer.Release(); + } +} + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.h b/src/video_core/renderer_opengl/frame_dumper_opengl.h new file mode 100644 index 000000000..da6d96053 --- /dev/null +++ b/src/video_core/renderer_opengl/frame_dumper_opengl.h @@ -0,0 +1,57 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "core/dumping/backend.h" +#include "core/frontend/framebuffer_layout.h" +#include "video_core/renderer_opengl/gl_resource_manager.h" + +namespace Frontend { +class EmuWindow; +class GraphicsContext; +class TextureMailbox; +} // namespace Frontend + +namespace OpenGL { + +class RendererOpenGL; + +/** + * This is the 'presentation' part in frame dumping. + * Processes frames/textures sent to its mailbox, downloads the pixels and sends the data + * to the video encoding backend. + */ +class FrameDumperOpenGL { +public: + explicit FrameDumperOpenGL(VideoDumper::Backend& video_dumper, Frontend::EmuWindow& emu_window); + ~FrameDumperOpenGL(); + + bool IsDumping() const; + Layout::FramebufferLayout GetLayout() const; + void StartDumping(); + void StopDumping(); + + std::unique_ptr mailbox; + +private: + void InitializeOpenGLObjects(); + void CleanupOpenGLObjects(); + void PresentLoop(); + + VideoDumper::Backend& video_dumper; + std::unique_ptr context; + std::thread present_thread; + std::atomic_bool stop_requested{false}; + + // PBOs used to dump frames faster + std::array pbos; + GLuint current_pbo = 1; + GLuint next_pbo = 0; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_format_reinterpreter.cpp b/src/video_core/renderer_opengl/gl_format_reinterpreter.cpp new file mode 100644 index 000000000..2175c62bd --- /dev/null +++ b/src/video_core/renderer_opengl/gl_format_reinterpreter.cpp @@ -0,0 +1,238 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/assert.h" +#include "common/scope_exit.h" +#include "video_core/renderer_opengl/gl_format_reinterpreter.h" +#include "video_core/renderer_opengl/gl_rasterizer_cache.h" +#include "video_core/renderer_opengl/gl_state.h" +#include "video_core/renderer_opengl/gl_vars.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" + +namespace OpenGL { + +using PixelFormat = SurfaceParams::PixelFormat; + +class RGBA4toRGB5A1 final : public FormatReinterpreterBase { +public: + RGBA4toRGB5A1() { + constexpr std::string_view vs_source = R"( +out vec2 dst_coord; + +uniform mediump ivec2 dst_size; + +const vec2 vertices[4] = + vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0)); + +void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + dst_coord = (vertices[gl_VertexID] / 2.0 + 0.5) * vec2(dst_size); +} +)"; + + constexpr std::string_view fs_source = R"( +in mediump vec2 dst_coord; + +out lowp vec4 frag_color; + +uniform lowp sampler2D source; +uniform mediump ivec2 dst_size; +uniform mediump ivec2 src_size; +uniform mediump ivec2 src_offset; + +void main() { + mediump ivec2 tex_coord; + if (src_size == dst_size) { + tex_coord = ivec2(dst_coord); + } else { + highp int tex_index = int(dst_coord.y) * dst_size.x + int(dst_coord.x); + mediump int y = tex_index / src_size.x; + tex_coord = ivec2(tex_index - y * src_size.x, y); + } + tex_coord -= src_offset; + + lowp ivec4 rgba4 = ivec4(texelFetch(source, tex_coord, 0) * (exp2(4.0) - 1.0)); + lowp ivec3 rgb5 = + ((rgba4.rgb << ivec3(1, 2, 3)) | (rgba4.gba >> ivec3(3, 2, 1))) & 0x1F; + frag_color = vec4(vec3(rgb5) / (exp2(5.0) - 1.0), rgba4.a & 0x01); +} +)"; + + program.Create(vs_source.data(), fs_source.data()); + dst_size_loc = glGetUniformLocation(program.handle, "dst_size"); + src_size_loc = glGetUniformLocation(program.handle, "src_size"); + src_offset_loc = glGetUniformLocation(program.handle, "src_offset"); + vao.Create(); + } + + void Reinterpret(GLuint src_tex, const Common::Rectangle& src_rect, GLuint read_fb_handle, + GLuint dst_tex, const Common::Rectangle& dst_rect, + GLuint draw_fb_handle) override { + OpenGLState prev_state = OpenGLState::GetCurState(); + SCOPE_EXIT({ prev_state.Apply(); }); + + OpenGLState state; + state.texture_units[0].texture_2d = src_tex; + state.draw.draw_framebuffer = draw_fb_handle; + state.draw.shader_program = program.handle; + state.draw.vertex_array = vao.handle; + state.viewport = {static_cast(dst_rect.left), static_cast(dst_rect.bottom), + static_cast(dst_rect.GetWidth()), + static_cast(dst_rect.GetHeight())}; + state.Apply(); + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, + 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, + 0); + + glUniform2i(dst_size_loc, dst_rect.GetWidth(), dst_rect.GetHeight()); + glUniform2i(src_size_loc, src_rect.GetWidth(), src_rect.GetHeight()); + glUniform2i(src_offset_loc, src_rect.left, src_rect.bottom); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + } + +private: + OGLProgram program; + GLint dst_size_loc{-1}, src_size_loc{-1}, src_offset_loc{-1}; + OGLVertexArray vao; +}; + +class PixelBufferD24S8toABGR final : public FormatReinterpreterBase { +public: + PixelBufferD24S8toABGR() { + attributeless_vao.Create(); + d24s8_abgr_buffer.Create(); + d24s8_abgr_buffer_size = 0; + + constexpr std::string_view vs_source = R"( +const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), + vec2(-1.0, 1.0), vec2(1.0, 1.0)); +void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); +} +)"; + + std::string fs_source = GLES ? fragment_shader_precision_OES : ""; + fs_source += R"( +uniform samplerBuffer tbo; +uniform vec2 tbo_size; +uniform vec4 viewport; + +out vec4 color; + +void main() { + vec2 tbo_coord = (gl_FragCoord.xy - viewport.xy) * tbo_size / viewport.zw; + int tbo_offset = int(tbo_coord.y) * int(tbo_size.x) + int(tbo_coord.x); + color = texelFetch(tbo, tbo_offset).rabg; +} +)"; + d24s8_abgr_shader.Create(vs_source.data(), fs_source.c_str()); + + OpenGLState state = OpenGLState::GetCurState(); + GLuint old_program = state.draw.shader_program; + state.draw.shader_program = d24s8_abgr_shader.handle; + state.Apply(); + + GLint tbo_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo"); + ASSERT(tbo_u_id != -1); + glUniform1i(tbo_u_id, 0); + + state.draw.shader_program = old_program; + state.Apply(); + + d24s8_abgr_tbo_size_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo_size"); + ASSERT(d24s8_abgr_tbo_size_u_id != -1); + d24s8_abgr_viewport_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "viewport"); + ASSERT(d24s8_abgr_viewport_u_id != -1); + } + + ~PixelBufferD24S8toABGR() {} + + void Reinterpret(GLuint src_tex, const Common::Rectangle& src_rect, GLuint read_fb_handle, + GLuint dst_tex, const Common::Rectangle& dst_rect, + GLuint draw_fb_handle) override { + OpenGLState prev_state = OpenGLState::GetCurState(); + SCOPE_EXIT({ prev_state.Apply(); }); + + OpenGLState state; + state.draw.read_framebuffer = read_fb_handle; + state.draw.draw_framebuffer = draw_fb_handle; + state.Apply(); + + glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle); + + GLsizeiptr target_pbo_size = + static_cast(src_rect.GetWidth()) * src_rect.GetHeight() * 4; + if (target_pbo_size > d24s8_abgr_buffer_size) { + d24s8_abgr_buffer_size = target_pbo_size * 2; + glBufferData(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer_size, nullptr, GL_STREAM_COPY); + } + + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, + src_tex, 0); + glReadPixels(static_cast(src_rect.left), static_cast(src_rect.bottom), + static_cast(src_rect.GetWidth()), + static_cast(src_rect.GetHeight()), GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, 0); + + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + + // PBO now contains src_tex in RABG format + state.draw.shader_program = d24s8_abgr_shader.handle; + state.draw.vertex_array = attributeless_vao.handle; + state.viewport.x = static_cast(dst_rect.left); + state.viewport.y = static_cast(dst_rect.bottom); + state.viewport.width = static_cast(dst_rect.GetWidth()); + state.viewport.height = static_cast(dst_rect.GetHeight()); + state.Apply(); + + OGLTexture tbo; + tbo.Create(); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_BUFFER, tbo.handle); + glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, d24s8_abgr_buffer.handle); + + glUniform2f(d24s8_abgr_tbo_size_u_id, static_cast(src_rect.GetWidth()), + static_cast(src_rect.GetHeight())); + glUniform4f(d24s8_abgr_viewport_u_id, static_cast(state.viewport.x), + static_cast(state.viewport.y), + static_cast(state.viewport.width), + static_cast(state.viewport.height)); + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, + 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, + 0); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + glBindTexture(GL_TEXTURE_BUFFER, 0); + } + +private: + OGLVertexArray attributeless_vao; + OGLBuffer d24s8_abgr_buffer; + GLsizeiptr d24s8_abgr_buffer_size; + OGLProgram d24s8_abgr_shader; + GLint d24s8_abgr_tbo_size_u_id; + GLint d24s8_abgr_viewport_u_id; +}; + +FormatReinterpreterOpenGL::FormatReinterpreterOpenGL() { + reinterpreters.emplace(PixelFormatPair{PixelFormat::RGBA8, PixelFormat::D24S8}, + std::make_unique()); + reinterpreters.emplace(PixelFormatPair{PixelFormat::RGB5A1, PixelFormat::RGBA4}, + std::make_unique()); +} + +FormatReinterpreterOpenGL::~FormatReinterpreterOpenGL() = default; + +std::pair +FormatReinterpreterOpenGL::GetPossibleReinterpretations(PixelFormat dst_format) { + return reinterpreters.equal_range(dst_format); +} + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_format_reinterpreter.h b/src/video_core/renderer_opengl/gl_format_reinterpreter.h new file mode 100644 index 000000000..d4b544096 --- /dev/null +++ b/src/video_core/renderer_opengl/gl_format_reinterpreter.h @@ -0,0 +1,62 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/common_types.h" +#include "common/math_util.h" +#include "video_core/renderer_opengl/gl_resource_manager.h" +#include "video_core/renderer_opengl/gl_surface_params.h" + +namespace OpenGL { + +class RasterizerCacheOpenGL; + +struct PixelFormatPair { + const SurfaceParams::PixelFormat dst_format, src_format; + struct less { + using is_transparent = void; + constexpr bool operator()(OpenGL::PixelFormatPair lhs, OpenGL::PixelFormatPair rhs) const { + return std::tie(lhs.dst_format, lhs.src_format) < + std::tie(rhs.dst_format, rhs.src_format); + } + constexpr bool operator()(OpenGL::SurfaceParams::PixelFormat lhs, + OpenGL::PixelFormatPair rhs) const { + return lhs < rhs.dst_format; + } + constexpr bool operator()(OpenGL::PixelFormatPair lhs, + OpenGL::SurfaceParams::PixelFormat rhs) const { + return lhs.dst_format < rhs; + } + }; +}; + +class FormatReinterpreterBase { +public: + virtual ~FormatReinterpreterBase() = default; + + virtual void Reinterpret(GLuint src_tex, const Common::Rectangle& src_rect, + GLuint read_fb_handle, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint draw_fb_handle) = 0; +}; + +class FormatReinterpreterOpenGL : NonCopyable { + using ReinterpreterMap = + std::map, PixelFormatPair::less>; + +public: + explicit FormatReinterpreterOpenGL(); + ~FormatReinterpreterOpenGL(); + + std::pair GetPossibleReinterpretations( + SurfaceParams::PixelFormat dst_format); + +private: + ReinterpreterMap reinterpreters; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp index 165a1c8fa..040e6e477 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -31,10 +32,11 @@ #include "core/settings.h" #include "video_core/pica_state.h" #include "video_core/renderer_base.h" +#include "video_core/renderer_opengl/gl_format_reinterpreter.h" #include "video_core/renderer_opengl/gl_rasterizer_cache.h" #include "video_core/renderer_opengl/gl_state.h" #include "video_core/renderer_opengl/gl_vars.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" #include "video_core/utils.h" #include "video_core/video_core.h" @@ -493,125 +495,6 @@ static bool FillSurface(const Surface& surface, const u8* fill_data, return true; } -SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const { - SurfaceParams params = *this; - const u32 tiled_size = is_tiled ? 8 : 1; - const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size); - PAddr aligned_start = - addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes); - PAddr aligned_end = - addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes); - - if (aligned_end - aligned_start > stride_tiled_bytes) { - params.addr = aligned_start; - params.height = (aligned_end - aligned_start) / BytesInPixels(stride); - } else { - // 1 row - ASSERT(aligned_end - aligned_start == stride_tiled_bytes); - const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1); - aligned_start = - addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment); - aligned_end = - addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment); - params.addr = aligned_start; - params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size; - params.stride = params.width; - params.height = tiled_size; - } - params.UpdateParams(); - - return params; -} - -SurfaceInterval SurfaceParams::GetSubRectInterval(Common::Rectangle unscaled_rect) const { - if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) { - return {}; - } - - if (is_tiled) { - unscaled_rect.left = Common::AlignDown(unscaled_rect.left, 8) * 8; - unscaled_rect.bottom = Common::AlignDown(unscaled_rect.bottom, 8) / 8; - unscaled_rect.right = Common::AlignUp(unscaled_rect.right, 8) * 8; - unscaled_rect.top = Common::AlignUp(unscaled_rect.top, 8) / 8; - } - - const u32 stride_tiled = !is_tiled ? stride : stride * 8; - - const u32 pixel_offset = - stride_tiled * (!is_tiled ? unscaled_rect.bottom : (height / 8) - unscaled_rect.top) + - unscaled_rect.left; - - const u32 pixels = (unscaled_rect.GetHeight() - 1) * stride_tiled + unscaled_rect.GetWidth(); - - return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)}; -} - -Common::Rectangle SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const { - const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr); - - if (is_tiled) { - const int x0 = (begin_pixel_index % (stride * 8)) / 8; - const int y0 = (begin_pixel_index / (stride * 8)) * 8; - // Top to bottom - return Common::Rectangle(x0, height - y0, x0 + sub_surface.width, - height - (y0 + sub_surface.height)); - } - - const int x0 = begin_pixel_index % stride; - const int y0 = begin_pixel_index / stride; - // Bottom to top - return Common::Rectangle(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0); -} - -Common::Rectangle SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const { - auto rect = GetSubRect(sub_surface); - rect.left = rect.left * res_scale; - rect.right = rect.right * res_scale; - rect.top = rect.top * res_scale; - rect.bottom = rect.bottom * res_scale; - return rect; -} - -bool SurfaceParams::ExactMatch(const SurfaceParams& other_surface) const { - return std::tie(other_surface.addr, other_surface.width, other_surface.height, - other_surface.stride, other_surface.pixel_format, other_surface.is_tiled) == - std::tie(addr, width, height, stride, pixel_format, is_tiled) && - pixel_format != PixelFormat::Invalid; -} - -bool SurfaceParams::CanSubRect(const SurfaceParams& sub_surface) const { - return sub_surface.addr >= addr && sub_surface.end <= end && - sub_surface.pixel_format == pixel_format && pixel_format != PixelFormat::Invalid && - sub_surface.is_tiled == is_tiled && - (sub_surface.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 && - (sub_surface.stride == stride || sub_surface.height <= (is_tiled ? 8u : 1u)) && - GetSubRect(sub_surface).right <= stride; -} - -bool SurfaceParams::CanExpand(const SurfaceParams& expanded_surface) const { - return pixel_format != PixelFormat::Invalid && pixel_format == expanded_surface.pixel_format && - addr <= expanded_surface.end && expanded_surface.addr <= end && - is_tiled == expanded_surface.is_tiled && stride == expanded_surface.stride && - (std::max(expanded_surface.addr, addr) - std::min(expanded_surface.addr, addr)) % - BytesInPixels(stride * (is_tiled ? 8 : 1)) == - 0; -} - -bool SurfaceParams::CanTexCopy(const SurfaceParams& texcopy_params) const { - if (pixel_format == PixelFormat::Invalid || addr > texcopy_params.addr || - end < texcopy_params.end) { - return false; - } - if (texcopy_params.width != texcopy_params.stride) { - const u32 tile_stride = BytesInPixels(stride * (is_tiled ? 8 : 1)); - return (texcopy_params.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 && - texcopy_params.width % BytesInPixels(is_tiled ? 64 : 1) == 0 && - (texcopy_params.height == 1 || texcopy_params.stride == tile_stride) && - ((texcopy_params.addr - addr) % tile_stride) + texcopy_params.width <= tile_stride; - } - return FromInterval(texcopy_params.GetInterval()).GetInterval() == texcopy_params.GetInterval(); -} - bool CachedSurface::CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const { if (type == SurfaceType::Fill && IsRegionValid(fill_interval) && @@ -653,47 +536,6 @@ bool CachedSurface::CanCopy(const SurfaceParams& dest_surface, return false; } -SurfaceInterval SurfaceParams::GetCopyableInterval(const Surface& src_surface) const { - SurfaceInterval result{}; - const auto valid_regions = - SurfaceRegions(GetInterval() & src_surface->GetInterval()) - src_surface->invalid_regions; - for (auto& valid_interval : valid_regions) { - const SurfaceInterval aligned_interval{ - addr + Common::AlignUp(boost::icl::first(valid_interval) - addr, - BytesInPixels(is_tiled ? 8 * 8 : 1)), - addr + Common::AlignDown(boost::icl::last_next(valid_interval) - addr, - BytesInPixels(is_tiled ? 8 * 8 : 1))}; - - if (BytesInPixels(is_tiled ? 8 * 8 : 1) > boost::icl::length(valid_interval) || - boost::icl::length(aligned_interval) == 0) { - continue; - } - - // Get the rectangle within aligned_interval - const u32 stride_bytes = BytesInPixels(stride) * (is_tiled ? 8 : 1); - SurfaceInterval rect_interval{ - addr + Common::AlignUp(boost::icl::first(aligned_interval) - addr, stride_bytes), - addr + Common::AlignDown(boost::icl::last_next(aligned_interval) - addr, stride_bytes), - }; - if (boost::icl::first(rect_interval) > boost::icl::last_next(rect_interval)) { - // 1 row - rect_interval = aligned_interval; - } else if (boost::icl::length(rect_interval) == 0) { - // 2 rows that do not make a rectangle, return the larger one - const SurfaceInterval row1{boost::icl::first(aligned_interval), - boost::icl::first(rect_interval)}; - const SurfaceInterval row2{boost::icl::first(rect_interval), - boost::icl::last_next(aligned_interval)}; - rect_interval = (boost::icl::length(row1) > boost::icl::length(row2)) ? row1 : row2; - } - - if (boost::icl::length(rect_interval) > boost::icl::length(result)) { - result = rect_interval; - } - } - return result; -} - MICROPROFILE_DEFINE(OpenGL_CopySurface, "OpenGL", "CopySurface", MP_RGB(128, 192, 64)); void RasterizerCacheOpenGL::CopySurface(const Surface& src_surface, const Surface& dst_surface, SurfaceInterval copy_interval) { @@ -884,6 +726,16 @@ bool CachedSurface::LoadCustomTexture(u64 tex_hash, Core::CustomTexInfo& tex_inf } void CachedSurface::DumpTexture(GLuint target_tex, u64 tex_hash) { + // Make sure the texture size is a power of 2 + // If not, the surface is actually a framebuffer + std::bitset<32> width_bits(width); + std::bitset<32> height_bits(height); + if (width_bits.count() != 1 || height_bits.count() != 1) { + LOG_WARNING(Render_OpenGL, "Not dumping {:016X} because size isn't a power of 2 ({}x{})", + tex_hash, width, height); + return; + } + // Dump texture to RGBA8 and encode as PNG const auto& image_interface = Core::System::GetInstance().GetImageInterface(); auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache(); @@ -945,10 +797,6 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ if (Settings::values.custom_textures) is_custom = LoadCustomTexture(tex_hash, custom_tex_info); - TextureFilterInterface* const texture_filter = - is_custom ? nullptr : TextureFilterManager::GetInstance().GetTextureFilter(); - const u16 default_scale = texture_filter ? texture_filter->scale_factor : 1; - // Load data from memory to the surface GLint x0 = static_cast(rect.left); GLint y0 = static_cast(rect.bottom); @@ -960,7 +808,7 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ // If not 1x scale, create 1x texture that we will blit from to replace texture subrect in // surface OGLTexture unscaled_tex; - if (res_scale != default_scale) { + if (res_scale != 1) { x0 = 0; y0 = 0; @@ -969,8 +817,7 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ AllocateSurfaceTexture(unscaled_tex.handle, GetFormatTuple(PixelFormat::RGBA8), custom_tex_info.width, custom_tex_info.height); } else { - AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth() * default_scale, - rect.GetHeight() * default_scale); + AllocateSurfaceTexture(unscaled_tex.handle, tuple, rect.GetWidth(), rect.GetHeight()); } target_tex = unscaled_tex.handle; } @@ -996,16 +843,6 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ 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 if (texture_filter) { - if (res_scale == default_scale) { - AllocateSurfaceTexture(texture.handle, GetFormatTuple(pixel_format), - rect.GetWidth() * default_scale, - rect.GetHeight() * default_scale); - cur_state.texture_units[0].texture_2d = texture.handle; - cur_state.Apply(); - } - texture_filter->scale(*this, {(u32)x0, (u32)y0, rect.GetWidth(), rect.GetHeight()}, - buffer_offset); } else { glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(stride)); @@ -1016,13 +853,13 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ } glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - if (Settings::values.dump_textures && !is_custom && !texture_filter) + 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 != default_scale) { + if (res_scale != 1) { auto scaled_rect = rect; scaled_rect.left *= res_scale; scaled_rect.top *= res_scale; @@ -1031,8 +868,11 @@ void CachedSurface::UploadGLTexture(Common::Rectangle rect, GLuint read_fb_ auto from_rect = is_custom ? Common::Rectangle{0, custom_tex_info.height, custom_tex_info.width, 0} : Common::Rectangle{0, rect.GetHeight(), rect.GetWidth(), 0}; - BlitTextures(unscaled_tex.handle, from_rect, texture.handle, scaled_rect, type, - read_fb_handle, draw_fb_handle); + if (!owner.texture_filterer->Filter(unscaled_tex.handle, from_rect, texture.handle, + scaled_rect, type, read_fb_handle, draw_fb_handle)) { + BlitTextures(unscaled_tex.handle, from_rect, texture.handle, scaled_rect, type, + read_fb_handle, draw_fb_handle); + } } InvalidateAllWatcher(); @@ -1221,53 +1061,13 @@ Surface FindMatch(const SurfaceCache& surface_cache, const SurfaceParams& params } RasterizerCacheOpenGL::RasterizerCacheOpenGL() { + resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); + texture_filterer = std::make_unique(Settings::values.texture_filter_name, + resolution_scale_factor); + format_reinterpreter = std::make_unique(); + read_framebuffer.Create(); draw_framebuffer.Create(); - - attributeless_vao.Create(); - - d24s8_abgr_buffer.Create(); - d24s8_abgr_buffer_size = 0; - - std::string vs_source = R"( -const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0)); -void main() { - gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); -} -)"; - - std::string fs_source = GLES ? fragment_shader_precision_OES : ""; - fs_source += R"( -uniform samplerBuffer tbo; -uniform vec2 tbo_size; -uniform vec4 viewport; - -out vec4 color; - -void main() { - vec2 tbo_coord = (gl_FragCoord.xy - viewport.xy) * tbo_size / viewport.zw; - int tbo_offset = int(tbo_coord.y) * int(tbo_size.x) + int(tbo_coord.x); - color = texelFetch(tbo, tbo_offset).rabg; -} -)"; - d24s8_abgr_shader.Create(vs_source.c_str(), fs_source.c_str()); - - OpenGLState state = OpenGLState::GetCurState(); - GLuint old_program = state.draw.shader_program; - state.draw.shader_program = d24s8_abgr_shader.handle; - state.Apply(); - - GLint tbo_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo"); - ASSERT(tbo_u_id != -1); - glUniform1i(tbo_u_id, 0); - - state.draw.shader_program = old_program; - state.Apply(); - - d24s8_abgr_tbo_size_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "tbo_size"); - ASSERT(d24s8_abgr_tbo_size_u_id != -1); - d24s8_abgr_viewport_u_id = glGetUniformLocation(d24s8_abgr_shader.handle, "viewport"); - ASSERT(d24s8_abgr_viewport_u_id != -1); } RasterizerCacheOpenGL::~RasterizerCacheOpenGL() { @@ -1291,64 +1091,6 @@ bool RasterizerCacheOpenGL::BlitSurfaces(const Surface& src_surface, draw_framebuffer.handle); } -void RasterizerCacheOpenGL::ConvertD24S8toABGR(GLuint src_tex, - const Common::Rectangle& src_rect, - GLuint dst_tex, - const Common::Rectangle& dst_rect) { - OpenGLState prev_state = OpenGLState::GetCurState(); - SCOPE_EXIT({ prev_state.Apply(); }); - - OpenGLState state; - state.draw.read_framebuffer = read_framebuffer.handle; - state.draw.draw_framebuffer = draw_framebuffer.handle; - state.Apply(); - - glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle); - - GLsizeiptr target_pbo_size = src_rect.GetWidth() * src_rect.GetHeight() * 4; - if (target_pbo_size > d24s8_abgr_buffer_size) { - d24s8_abgr_buffer_size = target_pbo_size * 2; - glBufferData(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer_size, nullptr, GL_STREAM_COPY); - } - - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, src_tex, - 0); - glReadPixels(static_cast(src_rect.left), static_cast(src_rect.bottom), - static_cast(src_rect.GetWidth()), - static_cast(src_rect.GetHeight()), GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, - 0); - - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - - // PBO now contains src_tex in RABG format - state.draw.shader_program = d24s8_abgr_shader.handle; - state.draw.vertex_array = attributeless_vao.handle; - state.viewport.x = static_cast(dst_rect.left); - state.viewport.y = static_cast(dst_rect.bottom); - state.viewport.width = static_cast(dst_rect.GetWidth()); - state.viewport.height = static_cast(dst_rect.GetHeight()); - state.Apply(); - - OGLTexture tbo; - tbo.Create(); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_BUFFER, tbo.handle); - glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, d24s8_abgr_buffer.handle); - - glUniform2f(d24s8_abgr_tbo_size_u_id, static_cast(src_rect.GetWidth()), - static_cast(src_rect.GetHeight())); - glUniform4f(d24s8_abgr_viewport_u_id, static_cast(state.viewport.x), - static_cast(state.viewport.y), static_cast(state.viewport.width), - static_cast(state.viewport.height)); - - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - glBindTexture(GL_TEXTURE_BUFFER, 0); -} - Surface RasterizerCacheOpenGL::GetSurface(const SurfaceParams& params, ScaleMatch match_res_scale, bool load_if_create) { if (params.addr == 0 || params.height * params.width == 0) { @@ -1495,11 +1237,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf params.height = info.height; params.is_tiled = true; params.pixel_format = SurfaceParams::PixelFormatFromTextureFormat(info.format); - TextureFilterInterface* filter{}; - - params.res_scale = (filter = TextureFilterManager::GetInstance().GetTextureFilter()) - ? filter->scale_factor - : 1; + params.res_scale = texture_filterer->IsNull() ? 1 : resolution_scale_factor; params.UpdateParams(); u32 min_width = info.width >> max_level; @@ -1552,7 +1290,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf 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) { + if (surface->is_custom || !texture_filterer->IsNull()) { // TODO: proper mipmap support for custom textures glGenerateMipmap(GL_TEXTURE_2D); } @@ -1588,7 +1326,7 @@ Surface RasterizerCacheOpenGL::GetTextureSurface(const Pica::Texture::TextureInf } state.ResetTexture(level_surface->texture.handle); state.Apply(); - if (!surface->is_custom) { + if (!surface->is_custom && texture_filterer->IsNull()) { glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, level_surface->texture.handle, 0); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, @@ -1712,10 +1450,9 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces( const auto& config = regs.framebuffer.framebuffer; // update resolution_scale_factor and reset cache if changed - static u16 resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); - if (resolution_scale_factor != VideoCore::GetResolutionScaleFactor() || - TextureFilterManager::GetInstance().IsUpdated()) { - TextureFilterManager::GetInstance().Reset(); + if ((resolution_scale_factor != VideoCore::GetResolutionScaleFactor()) | + (VideoCore::g_texture_filter_update_requested.exchange(false) && + texture_filterer->Reset(Settings::values.texture_filter_name, resolution_scale_factor))) { resolution_scale_factor = VideoCore::GetResolutionScaleFactor(); FlushAll(); while (!surface_cache.empty()) @@ -1800,7 +1537,7 @@ SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces( } Surface RasterizerCacheOpenGL::GetFillSurface(const GPU::Regs::MemoryFillConfig& config) { - Surface new_surface = std::make_shared(); + Surface new_surface = std::make_shared(*this); new_surface->addr = config.GetStartAddress(); new_surface->end = config.GetEndAddress(); @@ -1881,9 +1618,15 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr, return; } + auto validate_regions = surface->invalid_regions & validate_interval; + auto notify_validated = [&](SurfaceInterval interval) { + surface->invalid_regions.erase(interval); + validate_regions.erase(interval); + }; + while (true) { - const auto it = surface->invalid_regions.find(validate_interval); - if (it == surface->invalid_regions.end()) + const auto it = validate_regions.begin(); + if (it == validate_regions.end()) break; const auto interval = *it & validate_interval; @@ -1895,27 +1638,27 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr, if (copy_surface != nullptr) { SurfaceInterval copy_interval = params.GetCopyableInterval(copy_surface); CopySurface(copy_surface, surface, copy_interval); - surface->invalid_regions.erase(copy_interval); + notify_validated(copy_interval); continue; } - // D24S8 to RGBA8 - if (surface->pixel_format == PixelFormat::RGBA8) { - params.pixel_format = PixelFormat::D24S8; - Surface reinterpret_surface = - FindMatch(surface_cache, params, ScaleMatch::Ignore, interval); - if (reinterpret_surface != nullptr) { - ASSERT(reinterpret_surface->pixel_format == PixelFormat::D24S8); - - SurfaceInterval convert_interval = params.GetCopyableInterval(reinterpret_surface); - SurfaceParams convert_params = surface->FromInterval(convert_interval); - auto src_rect = reinterpret_surface->GetScaledSubRect(convert_params); - auto dest_rect = surface->GetScaledSubRect(convert_params); - - ConvertD24S8toABGR(reinterpret_surface->texture.handle, src_rect, - surface->texture.handle, dest_rect); - - surface->invalid_regions.erase(convert_interval); + // Try to find surface in cache with different format + // that can can be reinterpreted to the requested format. + if (ValidateByReinterpretation(surface, params, interval)) { + notify_validated(interval); + continue; + } + // Could not find a matching reinterpreter, check if we need to implement a + // reinterpreter + if (NoUnimplementedReinterpretations(surface, params, interval) && + !IntervalHasInvalidPixelFormat(params, interval)) { + // No surfaces were found in the cache that had a matching bit-width. + // If the region was created entirely on the GPU, + // assume it was a developer mistake and skip flushing. + if (boost::icl::contains(dirty_regions, interval)) { + LOG_DEBUG(Render_OpenGL, "Region created fully on GPU and reinterpretation is " + "invalid. Skipping validation"); + validate_regions.erase(interval); continue; } } @@ -1925,10 +1668,103 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, PAddr addr, surface->LoadGLBuffer(params.addr, params.end); surface->UploadGLTexture(surface->GetSubRect(params), read_framebuffer.handle, draw_framebuffer.handle); - surface->invalid_regions.erase(params.GetInterval()); + notify_validated(params.GetInterval()); } } +bool RasterizerCacheOpenGL::NoUnimplementedReinterpretations(const Surface& surface, + SurfaceParams& params, + const SurfaceInterval& interval) { + static constexpr std::array all_formats{ + PixelFormat::RGBA8, PixelFormat::RGB8, PixelFormat::RGB5A1, PixelFormat::RGB565, + PixelFormat::RGBA4, PixelFormat::IA8, PixelFormat::RG8, PixelFormat::I8, + PixelFormat::A8, PixelFormat::IA4, PixelFormat::I4, PixelFormat::A4, + PixelFormat::ETC1, PixelFormat::ETC1A4, PixelFormat::D16, PixelFormat::D24, + PixelFormat::D24S8, + }; + bool implemented = true; + for (PixelFormat format : all_formats) { + if (SurfaceParams::GetFormatBpp(format) == surface->GetFormatBpp()) { + params.pixel_format = format; + // This could potentially be expensive, + // although experimentally it hasn't been too bad + Surface test_surface = + FindMatch(surface_cache, params, ScaleMatch::Ignore, interval); + if (test_surface != nullptr) { + LOG_WARNING(Render_OpenGL, "Missing pixel_format reinterpreter: {} -> {}", + SurfaceParams::PixelFormatAsString(format), + SurfaceParams::PixelFormatAsString(surface->pixel_format)); + implemented = false; + } + } + } + return implemented; +} + +bool RasterizerCacheOpenGL::IntervalHasInvalidPixelFormat(SurfaceParams& params, + const SurfaceInterval& interval) { + params.pixel_format = PixelFormat::Invalid; + for (const auto& set : RangeFromInterval(surface_cache, interval)) + for (const auto& surface : set.second) + if (surface->pixel_format == PixelFormat::Invalid) { + LOG_WARNING(Render_OpenGL, "Surface found with invalid pixel format"); + return true; + } + return false; +} + +bool RasterizerCacheOpenGL::ValidateByReinterpretation(const Surface& surface, + SurfaceParams& params, + const SurfaceInterval& interval) { + auto [cvt_begin, cvt_end] = + format_reinterpreter->GetPossibleReinterpretations(surface->pixel_format); + for (auto reinterpreter = cvt_begin; reinterpreter != cvt_end; ++reinterpreter) { + PixelFormat format = reinterpreter->first.src_format; + params.pixel_format = format; + Surface reinterpret_surface = + FindMatch(surface_cache, params, ScaleMatch::Ignore, interval); + + if (reinterpret_surface != nullptr) { + SurfaceInterval reinterpret_interval = params.GetCopyableInterval(reinterpret_surface); + SurfaceParams reinterpret_params = surface->FromInterval(reinterpret_interval); + auto src_rect = reinterpret_surface->GetScaledSubRect(reinterpret_params); + auto dest_rect = surface->GetScaledSubRect(reinterpret_params); + + if (!texture_filterer->IsNull() && reinterpret_surface->res_scale == 1 && + surface->res_scale == resolution_scale_factor) { + // The destination surface is either a framebuffer, or a filtered texture. + OGLTexture tmp_tex; + tmp_tex.Create(); + // Create an intermediate surface to convert to before blitting to the + // destination. + Common::Rectangle tmp_rect{0, dest_rect.GetHeight() / resolution_scale_factor, + dest_rect.GetWidth() / resolution_scale_factor, 0}; + AllocateSurfaceTexture(tmp_tex.handle, + GetFormatTuple(reinterpreter->first.dst_format), + tmp_rect.right, tmp_rect.top); + reinterpreter->second->Reinterpret(reinterpret_surface->texture.handle, src_rect, + read_framebuffer.handle, tmp_tex.handle, + tmp_rect, draw_framebuffer.handle); + SurfaceParams::SurfaceType type = + SurfaceParams::GetFormatType(reinterpreter->first.dst_format); + + if (!texture_filterer->Filter(tmp_tex.handle, tmp_rect, surface->texture.handle, + dest_rect, type, read_framebuffer.handle, + draw_framebuffer.handle)) { + BlitTextures(tmp_tex.handle, tmp_rect, surface->texture.handle, dest_rect, type, + read_framebuffer.handle, draw_framebuffer.handle); + } + } else { + reinterpreter->second->Reinterpret(reinterpret_surface->texture.handle, src_rect, + read_framebuffer.handle, surface->texture.handle, + dest_rect, draw_framebuffer.handle); + } + return true; + } + } + return false; +} + void RasterizerCacheOpenGL::ClearAll(bool flush) { const auto flush_interval = PageMap::interval_type::right_open(0x0, 0xFFFFFFFF); // Force flush all surfaces from the cache @@ -2053,7 +1889,7 @@ void RasterizerCacheOpenGL::InvalidateRegion(PAddr addr, u32 size, const Surface } Surface RasterizerCacheOpenGL::CreateSurface(const SurfaceParams& params) { - Surface surface = std::make_shared(); + Surface surface = std::make_shared(*this); static_cast(*surface) = params; surface->texture.Create(); diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.h b/src/video_core/renderer_opengl/gl_rasterizer_cache.h index a4300f12a..673beb449 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer_cache.h +++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.h @@ -26,14 +26,16 @@ #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" #include "video_core/renderer_opengl/gl_resource_manager.h" +#include "video_core/renderer_opengl/gl_surface_params.h" #include "video_core/texture/texture_decode.h" namespace OpenGL { +class RasterizerCacheOpenGL; +class TextureFilterer; +class FormatReinterpreterOpenGL; + struct TextureCubeConfig { PAddr px; PAddr nx; @@ -76,11 +78,8 @@ struct hash { namespace OpenGL { -struct CachedSurface; -using Surface = std::shared_ptr; using SurfaceSet = std::set; -using SurfaceInterval = boost::icl::right_open_interval; using SurfaceRegions = boost::icl::interval_set; using SurfaceMap = boost::icl::interval_map BPP_TABLE = { - 32, // RGBA8 - 24, // RGB8 - 16, // RGB5A1 - 16, // RGB565 - 16, // RGBA4 - 16, // IA8 - 16, // RG8 - 8, // I8 - 8, // A8 - 8, // IA4 - 4, // I4 - 4, // A4 - 4, // ETC1 - 8, // ETC1A4 - 16, // D16 - 0, - 24, // D24 - 32, // D24S8 - }; - -public: - enum class PixelFormat { - // First 5 formats are shared between textures and color buffers - RGBA8 = 0, - RGB8 = 1, - RGB5A1 = 2, - RGB565 = 3, - RGBA4 = 4, - - // Texture-only formats - IA8 = 5, - RG8 = 6, - I8 = 7, - A8 = 8, - IA4 = 9, - I4 = 10, - A4 = 11, - ETC1 = 12, - ETC1A4 = 13, - - // Depth buffer-only formats - D16 = 14, - // gap - D24 = 16, - D24S8 = 17, - - Invalid = 255, - }; - - enum class SurfaceType { - Color = 0, - Texture = 1, - Depth = 2, - DepthStencil = 3, - Fill = 4, - Invalid = 5 - }; - - static constexpr unsigned int GetFormatBpp(PixelFormat format) { - const auto format_idx = static_cast(format); - DEBUG_ASSERT_MSG(format_idx < BPP_TABLE.size(), "Invalid pixel format {}", format_idx); - return BPP_TABLE[format_idx]; - } - - unsigned int GetFormatBpp() const { - return GetFormatBpp(pixel_format); - } - - static PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) { - return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid; - } - - static PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) { - return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid; - } - - static PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) { - return ((unsigned int)format < 4) ? (PixelFormat)((unsigned int)format + 14) - : PixelFormat::Invalid; - } - - static PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) { - switch (format) { - // RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat - case GPU::Regs::PixelFormat::RGB565: - return PixelFormat::RGB565; - case GPU::Regs::PixelFormat::RGB5A1: - return PixelFormat::RGB5A1; - default: - return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid; - } - } - - static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) { - SurfaceType a_type = GetFormatType(pixel_format_a); - SurfaceType b_type = GetFormatType(pixel_format_b); - - if ((a_type == SurfaceType::Color || a_type == SurfaceType::Texture) && - (b_type == SurfaceType::Color || b_type == SurfaceType::Texture)) { - return true; - } - - if (a_type == SurfaceType::Depth && b_type == SurfaceType::Depth) { - return true; - } - - if (a_type == SurfaceType::DepthStencil && b_type == SurfaceType::DepthStencil) { - return true; - } - - return false; - } - - static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) { - if ((unsigned int)pixel_format < 5) { - return SurfaceType::Color; - } - - if ((unsigned int)pixel_format < 14) { - return SurfaceType::Texture; - } - - if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) { - return SurfaceType::Depth; - } - - if (pixel_format == PixelFormat::D24S8) { - return SurfaceType::DepthStencil; - } - - return SurfaceType::Invalid; - } - - /// Update the params "size", "end" and "type" from the already set "addr", "width", "height" - /// and "pixel_format" - void UpdateParams() { - if (stride == 0) { - stride = width; - } - type = GetFormatType(pixel_format); - size = !is_tiled ? BytesInPixels(stride * (height - 1) + width) - : BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8); - end = addr + size; - } - - SurfaceInterval GetInterval() const { - return SurfaceInterval(addr, end); - } - - // Returns the outer rectangle containing "interval" - SurfaceParams FromInterval(SurfaceInterval interval) const; - - SurfaceInterval GetSubRectInterval(Common::Rectangle unscaled_rect) const; - - // Returns the region of the biggest valid rectange within interval - SurfaceInterval GetCopyableInterval(const Surface& src_surface) const; - - u32 GetScaledWidth() const { - return width * res_scale; - } - - u32 GetScaledHeight() const { - return height * res_scale; - } - - Common::Rectangle GetRect() const { - return {0, height, width, 0}; - } - - Common::Rectangle GetScaledRect() const { - return {0, GetScaledHeight(), GetScaledWidth(), 0}; - } - - u32 PixelsInBytes(u32 size) const { - return size * CHAR_BIT / GetFormatBpp(pixel_format); - } - - u32 BytesInPixels(u32 pixels) const { - return pixels * GetFormatBpp(pixel_format) / CHAR_BIT; - } - - bool ExactMatch(const SurfaceParams& other_surface) const; - bool CanSubRect(const SurfaceParams& sub_surface) const; - bool CanExpand(const SurfaceParams& expanded_surface) const; - bool CanTexCopy(const SurfaceParams& texcopy_params) const; - - Common::Rectangle GetSubRect(const SurfaceParams& sub_surface) const; - Common::Rectangle GetScaledSubRect(const SurfaceParams& sub_surface) const; - - PAddr addr = 0; - PAddr end = 0; - u32 size = 0; - - u32 width = 0; - u32 height = 0; - u32 stride = 0; - u16 res_scale = 1; - - bool is_tiled = false; - PixelFormat pixel_format = PixelFormat::Invalid; - SurfaceType type = SurfaceType::Invalid; -}; - /** * A watcher that notifies whether a cached surface has been changed. This is useful for caching * surface collection objects, including texture cube and mipmap. @@ -345,6 +138,8 @@ private: }; struct CachedSurface : SurfaceParams, std::enable_shared_from_this { + CachedSurface(RasterizerCacheOpenGL& owner) : owner{owner} {} + bool CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const; bool CanCopy(const SurfaceParams& dest_surface, SurfaceInterval copy_interval) const; @@ -422,6 +217,7 @@ struct CachedSurface : SurfaceParams, std::enable_shared_from_this> watchers; }; @@ -445,9 +241,6 @@ public: bool BlitSurfaces(const Surface& src_surface, const Common::Rectangle& src_rect, const Surface& dst_surface, const Common::Rectangle& dst_rect); - void ConvertD24S8toABGR(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, - const Common::Rectangle& dst_rect); - /// Copy one surface's region to another void CopySurface(const Surface& src_surface, const Surface& dst_surface, SurfaceInterval copy_interval); @@ -496,6 +289,18 @@ private: /// Update surface's texture for given region when necessary void ValidateSurface(const Surface& surface, PAddr addr, u32 size); + // Returns false if there is a surface in the cache at the interval with the same bit-width, + bool NoUnimplementedReinterpretations(const OpenGL::Surface& surface, + OpenGL::SurfaceParams& params, + const OpenGL::SurfaceInterval& interval); + + // Return true if a surface with an invalid pixel format exists at the interval + bool IntervalHasInvalidPixelFormat(SurfaceParams& params, const SurfaceInterval& interval); + + // Attempt to find a reinterpretable surface in the cache and use it to copy for validation + bool ValidateByReinterpretation(const Surface& surface, SurfaceParams& params, + const SurfaceInterval& interval); + /// Create a new surface Surface CreateSurface(const SurfaceParams& params); @@ -516,14 +321,13 @@ private: OGLFramebuffer read_framebuffer; OGLFramebuffer draw_framebuffer; - OGLVertexArray attributeless_vao; - OGLBuffer d24s8_abgr_buffer; - GLsizeiptr d24s8_abgr_buffer_size; - OGLProgram d24s8_abgr_shader; - GLint d24s8_abgr_tbo_size_u_id; - GLint d24s8_abgr_viewport_u_id; + u16 resolution_scale_factor; std::unordered_map texture_cube_cache; + +public: + std::unique_ptr texture_filterer; + std::unique_ptr format_reinterpreter; }; struct FormatTuple { diff --git a/src/video_core/renderer_opengl/gl_state.h b/src/video_core/renderer_opengl/gl_state.h index 5d655a127..cc7a864a2 100644 --- a/src/video_core/renderer_opengl/gl_state.h +++ b/src/video_core/renderer_opengl/gl_state.h @@ -38,13 +38,6 @@ constexpr GLuint ShadowTexturePZ = 5; constexpr GLuint ShadowTextureNZ = 6; } // namespace ImageUnits -struct Viewport { - GLint x; - GLint y; - GLsizei width; - GLsizei height; -}; - class OpenGLState { public: struct { @@ -142,7 +135,12 @@ public: GLsizei height; } scissor; - Viewport viewport; + struct { + GLint x; + GLint y; + GLsizei width; + GLsizei height; + } viewport; std::array clip_distance; // GL_CLIP_DISTANCE diff --git a/src/video_core/renderer_opengl/gl_surface_params.cpp b/src/video_core/renderer_opengl/gl_surface_params.cpp new file mode 100644 index 000000000..ceb0359ec --- /dev/null +++ b/src/video_core/renderer_opengl/gl_surface_params.cpp @@ -0,0 +1,171 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/alignment.h" +#include "video_core/renderer_opengl/gl_rasterizer_cache.h" +#include "video_core/renderer_opengl/gl_surface_params.h" + +namespace OpenGL { + +SurfaceParams SurfaceParams::FromInterval(SurfaceInterval interval) const { + SurfaceParams params = *this; + const u32 tiled_size = is_tiled ? 8 : 1; + const u32 stride_tiled_bytes = BytesInPixels(stride * tiled_size); + PAddr aligned_start = + addr + Common::AlignDown(boost::icl::first(interval) - addr, stride_tiled_bytes); + PAddr aligned_end = + addr + Common::AlignUp(boost::icl::last_next(interval) - addr, stride_tiled_bytes); + + if (aligned_end - aligned_start > stride_tiled_bytes) { + params.addr = aligned_start; + params.height = (aligned_end - aligned_start) / BytesInPixels(stride); + } else { + // 1 row + ASSERT(aligned_end - aligned_start == stride_tiled_bytes); + const u32 tiled_alignment = BytesInPixels(is_tiled ? 8 * 8 : 1); + aligned_start = + addr + Common::AlignDown(boost::icl::first(interval) - addr, tiled_alignment); + aligned_end = + addr + Common::AlignUp(boost::icl::last_next(interval) - addr, tiled_alignment); + params.addr = aligned_start; + params.width = PixelsInBytes(aligned_end - aligned_start) / tiled_size; + params.stride = params.width; + params.height = tiled_size; + } + params.UpdateParams(); + + return params; +} + +SurfaceInterval SurfaceParams::GetSubRectInterval(Common::Rectangle unscaled_rect) const { + if (unscaled_rect.GetHeight() == 0 || unscaled_rect.GetWidth() == 0) { + return {}; + } + + if (is_tiled) { + unscaled_rect.left = Common::AlignDown(unscaled_rect.left, 8) * 8; + unscaled_rect.bottom = Common::AlignDown(unscaled_rect.bottom, 8) / 8; + unscaled_rect.right = Common::AlignUp(unscaled_rect.right, 8) * 8; + unscaled_rect.top = Common::AlignUp(unscaled_rect.top, 8) / 8; + } + + const u32 stride_tiled = !is_tiled ? stride : stride * 8; + + const u32 pixel_offset = + stride_tiled * (!is_tiled ? unscaled_rect.bottom : (height / 8) - unscaled_rect.top) + + unscaled_rect.left; + + const u32 pixels = (unscaled_rect.GetHeight() - 1) * stride_tiled + unscaled_rect.GetWidth(); + + return {addr + BytesInPixels(pixel_offset), addr + BytesInPixels(pixel_offset + pixels)}; +} + +SurfaceInterval SurfaceParams::GetCopyableInterval(const Surface& src_surface) const { + SurfaceInterval result{}; + const auto valid_regions = + SurfaceRegions(GetInterval() & src_surface->GetInterval()) - src_surface->invalid_regions; + for (auto& valid_interval : valid_regions) { + const SurfaceInterval aligned_interval{ + addr + Common::AlignUp(boost::icl::first(valid_interval) - addr, + BytesInPixels(is_tiled ? 8 * 8 : 1)), + addr + Common::AlignDown(boost::icl::last_next(valid_interval) - addr, + BytesInPixels(is_tiled ? 8 * 8 : 1))}; + + if (BytesInPixels(is_tiled ? 8 * 8 : 1) > boost::icl::length(valid_interval) || + boost::icl::length(aligned_interval) == 0) { + continue; + } + + // Get the rectangle within aligned_interval + const u32 stride_bytes = BytesInPixels(stride) * (is_tiled ? 8 : 1); + SurfaceInterval rect_interval{ + addr + Common::AlignUp(boost::icl::first(aligned_interval) - addr, stride_bytes), + addr + Common::AlignDown(boost::icl::last_next(aligned_interval) - addr, stride_bytes), + }; + if (boost::icl::first(rect_interval) > boost::icl::last_next(rect_interval)) { + // 1 row + rect_interval = aligned_interval; + } else if (boost::icl::length(rect_interval) == 0) { + // 2 rows that do not make a rectangle, return the larger one + const SurfaceInterval row1{boost::icl::first(aligned_interval), + boost::icl::first(rect_interval)}; + const SurfaceInterval row2{boost::icl::first(rect_interval), + boost::icl::last_next(aligned_interval)}; + rect_interval = (boost::icl::length(row1) > boost::icl::length(row2)) ? row1 : row2; + } + + if (boost::icl::length(rect_interval) > boost::icl::length(result)) { + result = rect_interval; + } + } + return result; +} + +Common::Rectangle SurfaceParams::GetSubRect(const SurfaceParams& sub_surface) const { + const u32 begin_pixel_index = PixelsInBytes(sub_surface.addr - addr); + + if (is_tiled) { + const int x0 = (begin_pixel_index % (stride * 8)) / 8; + const int y0 = (begin_pixel_index / (stride * 8)) * 8; + // Top to bottom + return Common::Rectangle(x0, height - y0, x0 + sub_surface.width, + height - (y0 + sub_surface.height)); + } + + const int x0 = begin_pixel_index % stride; + const int y0 = begin_pixel_index / stride; + // Bottom to top + return Common::Rectangle(x0, y0 + sub_surface.height, x0 + sub_surface.width, y0); +} + +Common::Rectangle SurfaceParams::GetScaledSubRect(const SurfaceParams& sub_surface) const { + auto rect = GetSubRect(sub_surface); + rect.left = rect.left * res_scale; + rect.right = rect.right * res_scale; + rect.top = rect.top * res_scale; + rect.bottom = rect.bottom * res_scale; + return rect; +} + +bool SurfaceParams::ExactMatch(const SurfaceParams& other_surface) const { + return std::tie(other_surface.addr, other_surface.width, other_surface.height, + other_surface.stride, other_surface.pixel_format, other_surface.is_tiled) == + std::tie(addr, width, height, stride, pixel_format, is_tiled) && + pixel_format != PixelFormat::Invalid; +} + +bool SurfaceParams::CanSubRect(const SurfaceParams& sub_surface) const { + return sub_surface.addr >= addr && sub_surface.end <= end && + sub_surface.pixel_format == pixel_format && pixel_format != PixelFormat::Invalid && + sub_surface.is_tiled == is_tiled && + (sub_surface.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 && + (sub_surface.stride == stride || sub_surface.height <= (is_tiled ? 8u : 1u)) && + GetSubRect(sub_surface).right <= stride; +} + +bool SurfaceParams::CanExpand(const SurfaceParams& expanded_surface) const { + return pixel_format != PixelFormat::Invalid && pixel_format == expanded_surface.pixel_format && + addr <= expanded_surface.end && expanded_surface.addr <= end && + is_tiled == expanded_surface.is_tiled && stride == expanded_surface.stride && + (std::max(expanded_surface.addr, addr) - std::min(expanded_surface.addr, addr)) % + BytesInPixels(stride * (is_tiled ? 8 : 1)) == + 0; +} + +bool SurfaceParams::CanTexCopy(const SurfaceParams& texcopy_params) const { + if (pixel_format == PixelFormat::Invalid || addr > texcopy_params.addr || + end < texcopy_params.end) { + return false; + } + if (texcopy_params.width != texcopy_params.stride) { + const u32 tile_stride = BytesInPixels(stride * (is_tiled ? 8 : 1)); + return (texcopy_params.addr - addr) % BytesInPixels(is_tiled ? 64 : 1) == 0 && + texcopy_params.width % BytesInPixels(is_tiled ? 64 : 1) == 0 && + (texcopy_params.height == 1 || texcopy_params.stride == tile_stride) && + ((texcopy_params.addr - addr) % tile_stride) + texcopy_params.width <= tile_stride; + } + return FromInterval(texcopy_params.GetInterval()).GetInterval() == texcopy_params.GetInterval(); +} + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/gl_surface_params.h b/src/video_core/renderer_opengl/gl_surface_params.h new file mode 100644 index 000000000..e5e5e56f3 --- /dev/null +++ b/src/video_core/renderer_opengl/gl_surface_params.h @@ -0,0 +1,270 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/assert.h" +#include "common/math_util.h" +#include "core/hw/gpu.h" +#include "video_core/regs_framebuffer.h" +#include "video_core/regs_texturing.h" + +namespace OpenGL { + +struct CachedSurface; +using Surface = std::shared_ptr; + +using SurfaceInterval = boost::icl::right_open_interval; + +struct SurfaceParams { +private: + static constexpr std::array BPP_TABLE = { + 32, // RGBA8 + 24, // RGB8 + 16, // RGB5A1 + 16, // RGB565 + 16, // RGBA4 + 16, // IA8 + 16, // RG8 + 8, // I8 + 8, // A8 + 8, // IA4 + 4, // I4 + 4, // A4 + 4, // ETC1 + 8, // ETC1A4 + 16, // D16 + 0, + 24, // D24 + 32, // D24S8 + }; + +public: + enum class PixelFormat { + // First 5 formats are shared between textures and color buffers + RGBA8 = 0, + RGB8 = 1, + RGB5A1 = 2, + RGB565 = 3, + RGBA4 = 4, + + // Texture-only formats + IA8 = 5, + RG8 = 6, + I8 = 7, + A8 = 8, + IA4 = 9, + I4 = 10, + A4 = 11, + ETC1 = 12, + ETC1A4 = 13, + + // Depth buffer-only formats + D16 = 14, + // gap + D24 = 16, + D24S8 = 17, + + Invalid = 255, + }; + + enum class SurfaceType { + Color = 0, + Texture = 1, + Depth = 2, + DepthStencil = 3, + Fill = 4, + Invalid = 5 + }; + + static constexpr unsigned int GetFormatBpp(PixelFormat format) { + const auto format_idx = static_cast(format); + DEBUG_ASSERT_MSG(format_idx < BPP_TABLE.size(), "Invalid pixel format {}", format_idx); + return BPP_TABLE[format_idx]; + } + + unsigned int GetFormatBpp() const { + return GetFormatBpp(pixel_format); + } + + static std::string_view PixelFormatAsString(PixelFormat format) { + switch (format) { + case PixelFormat::RGBA8: + return "RGBA8"; + case PixelFormat::RGB8: + return "RGB8"; + case PixelFormat::RGB5A1: + return "RGB5A1"; + case PixelFormat::RGB565: + return "RGB565"; + case PixelFormat::RGBA4: + return "RGBA4"; + case PixelFormat::IA8: + return "IA8"; + case PixelFormat::RG8: + return "RG8"; + case PixelFormat::I8: + return "I8"; + case PixelFormat::A8: + return "A8"; + case PixelFormat::IA4: + return "IA4"; + case PixelFormat::I4: + return "I4"; + case PixelFormat::A4: + return "A4"; + case PixelFormat::ETC1: + return "ETC1"; + case PixelFormat::ETC1A4: + return "ETC1A4"; + case PixelFormat::D16: + return "D16"; + case PixelFormat::D24: + return "D24"; + case PixelFormat::D24S8: + return "D24S8"; + default: + return "Not a real pixel format"; + } + } + + static PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) { + return ((unsigned int)format < 14) ? (PixelFormat)format : PixelFormat::Invalid; + } + + static PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) { + return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid; + } + + static PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) { + return ((unsigned int)format < 4) ? (PixelFormat)((unsigned int)format + 14) + : PixelFormat::Invalid; + } + + static PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) { + switch (format) { + // RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat + case GPU::Regs::PixelFormat::RGB565: + return PixelFormat::RGB565; + case GPU::Regs::PixelFormat::RGB5A1: + return PixelFormat::RGB5A1; + default: + return ((unsigned int)format < 5) ? (PixelFormat)format : PixelFormat::Invalid; + } + } + + static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) { + SurfaceType a_type = GetFormatType(pixel_format_a); + SurfaceType b_type = GetFormatType(pixel_format_b); + + if ((a_type == SurfaceType::Color || a_type == SurfaceType::Texture) && + (b_type == SurfaceType::Color || b_type == SurfaceType::Texture)) { + return true; + } + + if (a_type == SurfaceType::Depth && b_type == SurfaceType::Depth) { + return true; + } + + if (a_type == SurfaceType::DepthStencil && b_type == SurfaceType::DepthStencil) { + return true; + } + + return false; + } + + static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) { + if ((unsigned int)pixel_format < 5) { + return SurfaceType::Color; + } + + if ((unsigned int)pixel_format < 14) { + return SurfaceType::Texture; + } + + if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) { + return SurfaceType::Depth; + } + + if (pixel_format == PixelFormat::D24S8) { + return SurfaceType::DepthStencil; + } + + return SurfaceType::Invalid; + } + + /// Update the params "size", "end" and "type" from the already set "addr", "width", "height" + /// and "pixel_format" + void UpdateParams() { + if (stride == 0) { + stride = width; + } + type = GetFormatType(pixel_format); + size = !is_tiled ? BytesInPixels(stride * (height - 1) + width) + : BytesInPixels(stride * 8 * (height / 8 - 1) + width * 8); + end = addr + size; + } + + SurfaceInterval GetInterval() const { + return SurfaceInterval(addr, end); + } + + // Returns the outer rectangle containing "interval" + SurfaceParams FromInterval(SurfaceInterval interval) const; + + SurfaceInterval GetSubRectInterval(Common::Rectangle unscaled_rect) const; + + // Returns the region of the biggest valid rectange within interval + SurfaceInterval GetCopyableInterval(const Surface& src_surface) const; + + u32 GetScaledWidth() const { + return width * res_scale; + } + + u32 GetScaledHeight() const { + return height * res_scale; + } + + Common::Rectangle GetRect() const { + return {0, height, width, 0}; + } + + Common::Rectangle GetScaledRect() const { + return {0, GetScaledHeight(), GetScaledWidth(), 0}; + } + + u32 PixelsInBytes(u32 size) const { + return size * CHAR_BIT / GetFormatBpp(pixel_format); + } + + u32 BytesInPixels(u32 pixels) const { + return pixels * GetFormatBpp(pixel_format) / CHAR_BIT; + } + + bool ExactMatch(const SurfaceParams& other_surface) const; + bool CanSubRect(const SurfaceParams& sub_surface) const; + bool CanExpand(const SurfaceParams& expanded_surface) const; + bool CanTexCopy(const SurfaceParams& texcopy_params) const; + + Common::Rectangle GetSubRect(const SurfaceParams& sub_surface) const; + Common::Rectangle GetScaledSubRect(const SurfaceParams& sub_surface) const; + + PAddr addr = 0; + PAddr end = 0; + u32 size = 0; + + u32 width = 0; + u32 height = 0; + u32 stride = 0; + u16 res_scale = 1; + + bool is_tiled = false; + PixelFormat pixel_format = PixelFormat::Invalid; + SurfaceType type = SurfaceType::Invalid; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index e7e0bb72f..b1fcfb592 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -32,23 +32,8 @@ #include "video_core/renderer_opengl/gl_vars.h" #include "video_core/renderer_opengl/post_processing_opengl.h" #include "video_core/renderer_opengl/renderer_opengl.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" #include "video_core/video_core.h" -namespace Frontend { - -struct Frame { - u32 width{}; /// Width of the frame (to detect resize) - u32 height{}; /// Height of the frame - bool color_reloaded = false; /// Texture attachment was recreated (ie: resized) - OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO - OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread - OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread - GLsync render_fence{}; /// Fence created on the render thread - GLsync present_fence{}; /// Fence created on the presentation thread -}; -} // namespace Frontend - namespace OpenGL { // If the size of this is too small, it ends up creating a soft cap on FPS as the renderer will have @@ -79,6 +64,7 @@ public: std::queue().swap(free_queue); present_queue.clear(); present_cv.notify_all(); + free_cv.notify_all(); } void ReloadPresentFrame(Frontend::Frame* frame, u32 height, u32 width) override { @@ -89,7 +75,7 @@ public: glBindFramebuffer(GL_FRAMEBUFFER, frame->present.handle); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, frame->color.handle); - if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { LOG_CRITICAL(Render_OpenGL, "Failed to recreate present FBO!"); } glBindFramebuffer(GL_DRAW_FRAMEBUFFER, previous_draw_fbo); @@ -115,7 +101,7 @@ public: state.Apply(); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, frame->color.handle); - if (!glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) { + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { LOG_CRITICAL(Render_OpenGL, "Failed to recreate render FBO!"); } prev_state.Apply(); @@ -145,19 +131,12 @@ public: present_cv.notify_one(); } - Frontend::Frame* TryGetPresentFrame(int timeout_ms) override { - std::unique_lock lock(swap_chain_lock); - // wait for new entries in the present_queue - present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), - [&] { return !present_queue.empty(); }); - if (present_queue.empty()) { - // timed out waiting for a frame to draw so return the previous frame - return previous_frame; - } - + // This is virtual as it is to be overriden in OGLVideoDumpingMailbox below. + virtual void LoadPresentFrame() { // free the previous frame and add it back to the free queue if (previous_frame) { free_queue.push(previous_frame); + free_cv.notify_one(); } // the newest entries are pushed to the front of the queue @@ -169,8 +148,72 @@ public: } present_queue.clear(); previous_frame = frame; + } + + Frontend::Frame* TryGetPresentFrame(int timeout_ms) override { + std::unique_lock lock(swap_chain_lock); + // wait for new entries in the present_queue + present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), + [&] { return !present_queue.empty(); }); + if (present_queue.empty()) { + // timed out waiting for a frame to draw so return the previous frame + return previous_frame; + } + + LoadPresentFrame(); + return previous_frame; + } +}; + +/// This mailbox is different in that it will never discard rendered frames +class OGLVideoDumpingMailbox : public OGLTextureMailbox { +public: + Frontend::Frame* GetRenderFrame() override { + std::unique_lock lock(swap_chain_lock); + + // If theres no free frames, we will wait until one shows up + if (free_queue.empty()) { + free_cv.wait(lock, [&] { return !free_queue.empty(); }); + } + + if (free_queue.empty()) { + LOG_CRITICAL(Render_OpenGL, "Could not get free frame"); + return nullptr; + } + + Frontend::Frame* frame = free_queue.front(); + free_queue.pop(); return frame; } + + void LoadPresentFrame() override { + // free the previous frame and add it back to the free queue + if (previous_frame) { + free_queue.push(previous_frame); + free_cv.notify_one(); + } + + Frontend::Frame* frame = present_queue.back(); + present_queue.pop_back(); + previous_frame = frame; + + // Do not remove entries from the present_queue, as video dumping would require + // that we preserve all frames + } + + Frontend::Frame* TryGetPresentFrame(int timeout_ms) override { + std::unique_lock lock(swap_chain_lock); + // wait for new entries in the present_queue + present_cv.wait_for(lock, std::chrono::milliseconds(timeout_ms), + [&] { return !present_queue.empty(); }); + if (present_queue.empty()) { + // timed out waiting for a frame + return nullptr; + } + + LoadPresentFrame(); + return previous_frame; + } }; static const char vertex_shader[] = R"( @@ -279,21 +322,35 @@ struct ScreenRectVertex { * * The projection part of the matrix is trivial, hence these operations are represented * by a 3x2 matrix. + * + * @param flipped Whether the frame should be flipped upside down. */ -static std::array MakeOrthographicMatrix(const float width, const float height) { +static std::array MakeOrthographicMatrix(const float width, const float height, + bool flipped) { + std::array matrix; // Laid out in column-major order - // clang-format off - matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f; - matrix[1] = 0.f; matrix[3] = -2.f / height; matrix[5] = 1.f; // Last matrix row is implicitly assumed to be [0, 0, 1]. - // clang-format on + if (flipped) { + // clang-format off + matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f; + matrix[1] = 0.f; matrix[3] = 2.f / height; matrix[5] = -1.f; + // clang-format on + } else { + // clang-format off + matrix[0] = 2.f / width; matrix[2] = 0.f; matrix[4] = -1.f; + matrix[1] = 0.f; matrix[3] = -2.f / height; matrix[5] = 1.f; + // clang-format on + } return matrix; } -RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) : RendererBase{window} { +RendererOpenGL::RendererOpenGL(Frontend::EmuWindow& window) + : RendererBase{window}, frame_dumper(Core::System::GetInstance().VideoDumper(), window) { + window.mailbox = std::make_unique(); + frame_dumper.mailbox = std::make_unique(); } RendererOpenGL::~RendererOpenGL() = default; @@ -311,56 +368,14 @@ void RendererOpenGL::SwapBuffers() { RenderScreenshot(); - RenderVideoDumping(); - const auto& layout = render_window.GetFramebufferLayout(); + RenderToMailbox(layout, render_window.mailbox, false); - Frontend::Frame* frame; - { - MICROPROFILE_SCOPE(OpenGL_WaitPresent); - - frame = render_window.mailbox->GetRenderFrame(); - - // Clean up sync objects before drawing - - // INTEL driver workaround. We can't delete the previous render sync object until we are - // sure that the presentation is done - if (frame->present_fence) { - glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); - } - - // delete the draw fence if the frame wasn't presented - if (frame->render_fence) { - glDeleteSync(frame->render_fence); - frame->render_fence = 0; - } - - // wait for the presentation to be done - if (frame->present_fence) { - glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(frame->present_fence); - frame->present_fence = 0; - } + if (frame_dumper.IsDumping()) { + RenderToMailbox(frame_dumper.GetLayout(), frame_dumper.mailbox, true); } - { - MICROPROFILE_SCOPE(OpenGL_RenderFrame); - // Recreate the frame if the size of the window has changed - if (layout.width != frame->width || layout.height != frame->height) { - LOG_DEBUG(Render_OpenGL, "Reloading render frame"); - render_window.mailbox->ReloadRenderFrame(frame, layout.width, layout.height); - } - - GLuint render_texture = frame->color.handle; - state.draw.draw_framebuffer = frame->render.handle; - state.Apply(); - DrawScreens(layout); - // Create a fence for the frontend to wait on and swap this frame to OffTex - frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); - render_window.mailbox->ReleaseRenderFrame(frame); - m_current_frame++; - } + m_current_frame++; Core::System::GetInstance().perf_stats->EndSystemFrame(); @@ -396,7 +411,7 @@ void RendererOpenGL::RenderScreenshot() { glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); - DrawScreens(layout); + DrawScreens(layout, false); glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, VideoCore::g_screenshot_bits); @@ -449,33 +464,54 @@ void RendererOpenGL::PrepareRendertarget() { } } -void RendererOpenGL::RenderVideoDumping() { - if (cleanup_video_dumping.exchange(false)) { - ReleaseVideoDumpingGLObjects(); - } +void RendererOpenGL::RenderToMailbox(const Layout::FramebufferLayout& layout, + std::unique_ptr& mailbox, + bool flipped) { - if (Core::System::GetInstance().VideoDumper().IsDumping()) { - if (prepare_video_dumping.exchange(false)) { - InitVideoDumpingGLObjects(); + Frontend::Frame* frame; + { + MICROPROFILE_SCOPE(OpenGL_WaitPresent); + + frame = mailbox->GetRenderFrame(); + + // Clean up sync objects before drawing + + // INTEL driver workaround. We can't delete the previous render sync object until we are + // sure that the presentation is done + if (frame->present_fence) { + glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); } - const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout(); - glBindFramebuffer(GL_READ_FRAMEBUFFER, frame_dumping_framebuffer.handle); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle); - DrawScreens(layout); + // delete the draw fence if the frame wasn't presented + if (frame->render_fence) { + glDeleteSync(frame->render_fence); + frame->render_fence = 0; + } - glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[current_pbo].handle); - glReadPixels(0, 0, layout.width, layout.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, 0); - glBindBuffer(GL_PIXEL_PACK_BUFFER, frame_dumping_pbos[next_pbo].handle); + // wait for the presentation to be done + if (frame->present_fence) { + glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(frame->present_fence); + frame->present_fence = 0; + } + } - GLubyte* pixels = static_cast(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY)); - VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels}; - Core::System::GetInstance().VideoDumper().AddVideoFrame(frame_data); + { + MICROPROFILE_SCOPE(OpenGL_RenderFrame); + // Recreate the frame if the size of the window has changed + if (layout.width != frame->width || layout.height != frame->height) { + LOG_DEBUG(Render_OpenGL, "Reloading render frame"); + mailbox->ReloadRenderFrame(frame, layout.width, layout.height); + } - glUnmapBuffer(GL_PIXEL_PACK_BUFFER); - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - current_pbo = (current_pbo + 1) % 2; - next_pbo = (current_pbo + 1) % 2; + GLuint render_texture = frame->color.handle; + state.draw.draw_framebuffer = frame->render.handle; + state.Apply(); + DrawScreens(layout, flipped); + // Create a fence for the frontend to wait on and swap this frame to OffTex + frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + mailbox->ReleaseRenderFrame(frame); } } @@ -886,7 +922,7 @@ void RendererOpenGL::DrawSingleScreenStereo(const ScreenInfo& screen_info_l, /** * Draws the emulated screens to the emulator window. */ -void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) { +void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool flipped) { if (VideoCore::g_renderer_bg_color_update_requested.exchange(false)) { // Update background color before drawing glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue, @@ -913,7 +949,7 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout) { // Set projection matrix std::array ortho_matrix = - MakeOrthographicMatrix((float)layout.width, (float)layout.height); + MakeOrthographicMatrix((float)layout.width, (float)layout.height, flipped); glUniformMatrix3x2fv(uniform_modelview_matrix, 1, GL_FALSE, ortho_matrix.data()); // Bind texture in Texture Unit 0 @@ -1052,41 +1088,11 @@ void RendererOpenGL::TryPresent(int timeout_ms) { void RendererOpenGL::UpdateFramerate() {} void RendererOpenGL::PrepareVideoDumping() { - prepare_video_dumping = true; + frame_dumper.StartDumping(); } void RendererOpenGL::CleanupVideoDumping() { - cleanup_video_dumping = true; -} - -void RendererOpenGL::InitVideoDumpingGLObjects() { - const auto& layout = Core::System::GetInstance().VideoDumper().GetLayout(); - - frame_dumping_framebuffer.Create(); - glGenRenderbuffers(1, &frame_dumping_renderbuffer); - glBindRenderbuffer(GL_RENDERBUFFER, frame_dumping_renderbuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, layout.width, layout.height); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frame_dumping_framebuffer.handle); - glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, - frame_dumping_renderbuffer); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - - for (auto& buffer : frame_dumping_pbos) { - buffer.Create(); - glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer.handle); - glBufferData(GL_PIXEL_PACK_BUFFER, layout.width * layout.height * 4, nullptr, - GL_STREAM_READ); - glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - } -} - -void RendererOpenGL::ReleaseVideoDumpingGLObjects() { - frame_dumping_framebuffer.Release(); - glDeleteRenderbuffers(1, &frame_dumping_renderbuffer); - - for (auto& buffer : frame_dumping_pbos) { - buffer.Release(); - } + frame_dumper.StopDumping(); } static const char* GetSource(GLenum source) { @@ -1179,14 +1185,10 @@ VideoCore::ResultStatus RendererOpenGL::Init() { RefreshRasterizerSetting(); - TextureFilterManager::GetInstance().Reset(); - return VideoCore::ResultStatus::Success; } /// Shutdown the renderer -void RendererOpenGL::ShutDown() { - TextureFilterManager::GetInstance().Destroy(); -} +void RendererOpenGL::ShutDown() {} } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index 96df7f8ac..634d26ca4 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -10,6 +10,7 @@ #include "common/math_util.h" #include "core/hw/gpu.h" #include "video_core/renderer_base.h" +#include "video_core/renderer_opengl/frame_dumper_opengl.h" #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" @@ -17,6 +18,20 @@ namespace Layout { struct FramebufferLayout; } +namespace Frontend { + +struct Frame { + u32 width{}; /// Width of the frame (to detect resize) + u32 height{}; /// Height of the frame + bool color_reloaded = false; /// Texture attachment was recreated (ie: resized) + OpenGL::OGLRenderbuffer color{}; /// Buffer shared between the render/present FBO + OpenGL::OGLFramebuffer render{}; /// FBO created on the render thread + OpenGL::OGLFramebuffer present{}; /// FBO created on the present thread + GLsync render_fence{}; /// Fence created on the render thread + GLsync present_fence{}; /// Fence created on the presentation thread +}; +} // namespace Frontend + namespace OpenGL { /// Structure used for storing information about the textures for each 3DS screen @@ -72,10 +87,11 @@ private: void ReloadShader(); void PrepareRendertarget(); void RenderScreenshot(); - void RenderVideoDumping(); + void RenderToMailbox(const Layout::FramebufferLayout& layout, + std::unique_ptr& mailbox, bool flipped); void ConfigureFramebufferTexture(TextureInfo& texture, const GPU::Regs::FramebufferConfig& framebuffer); - void DrawScreens(const Layout::FramebufferLayout& layout); + void DrawScreens(const Layout::FramebufferLayout& layout, bool flipped); void DrawSingleScreenRotated(const ScreenInfo& screen_info, float x, float y, float w, float h); void DrawSingleScreen(const ScreenInfo& screen_info, float x, float y, float w, float h); void DrawSingleScreenStereoRotated(const ScreenInfo& screen_info_l, @@ -91,9 +107,6 @@ private: // Fills active OpenGL texture with the given RGB color. void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b, const TextureInfo& texture); - void InitVideoDumpingGLObjects(); - void ReleaseVideoDumpingGLObjects(); - OpenGLState state; // OpenGL object IDs @@ -120,19 +133,7 @@ private: GLuint attrib_position; GLuint attrib_tex_coord; - // Frame dumping - OGLFramebuffer frame_dumping_framebuffer; - GLuint frame_dumping_renderbuffer; - - // Whether prepare/cleanup video dumping has been requested. - // They will be executed on next frame. - std::atomic_bool prepare_video_dumping = false; - std::atomic_bool cleanup_video_dumping = false; - - // PBOs used to dump frames faster - std::array frame_dumping_pbos; - GLuint current_pbo = 1; - GLuint next_pbo = 0; + FrameDumperOpenGL frame_dumper; }; } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp index 86b4a85b6..b70cc14f4 100644 --- a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp +++ b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.cpp @@ -42,18 +42,17 @@ namespace OpenGL { -Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(scale_factor) { +Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterBase(scale_factor) { const OpenGLState cur_state = OpenGLState::GetCurState(); - const auto setup_temp_tex = [this, scale_factor](TempTex& texture, GLint internal_format, - GLint format) { + const auto setup_temp_tex = [this](TempTex& texture, GLint internal_format, GLint format) { texture.fbo.Create(); texture.tex.Create(); state.draw.draw_framebuffer = texture.fbo.handle; state.Apply(); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_RECTANGLE, texture.tex.handle); - glTexImage2D(GL_TEXTURE_RECTANGLE, 0, internal_format, 1024 * scale_factor, - 1024 * scale_factor, 0, format, GL_HALF_FLOAT, nullptr); + glTexImage2D(GL_TEXTURE_RECTANGLE, 0, internal_format, 1024 * internal_scale_factor, + 1024 * internal_scale_factor, 0, format, GL_HALF_FLOAT, nullptr); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE, texture.tex.handle, 0); }; @@ -61,7 +60,6 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(sc setup_temp_tex(XY, GL_RG16F, GL_RG); vao.Create(); - out_fbo.Create(); for (std::size_t idx = 0; idx < samplers.size(); ++idx) { samplers[idx].Create(); @@ -86,30 +84,26 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterInterface(sc state.draw.shader_program = refine_program.handle; state.Apply(); glUniform1i(glGetUniformLocation(refine_program.handle, "LUMAD"), 1); + glUniform1f(glGetUniformLocation(refine_program.handle, "final_scale"), + static_cast(internal_scale_factor) / scale_factor); cur_state.Apply(); } -void Anime4kUltrafast::scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) { +void Anime4kUltrafast::Filter(GLuint src_tex, const Common::Rectangle& src_rect, + GLuint dst_tex, const Common::Rectangle& dst_rect, + GLuint read_fb_handle, GLuint draw_fb_handle) { const OpenGLState cur_state = OpenGLState::GetCurState(); - OGLTexture src_tex; - src_tex.Create(); - - state.viewport = RectToViewport(rect); - - state.texture_units[0].texture_2d = src_tex.handle; + state.viewport = {static_cast(src_rect.left * internal_scale_factor), + static_cast(src_rect.bottom * internal_scale_factor), + static_cast(src_rect.GetWidth() * internal_scale_factor), + static_cast(src_rect.GetHeight() * internal_scale_factor)}; + state.texture_units[0].texture_2d = src_tex; state.draw.draw_framebuffer = XY.fbo.handle; state.draw.shader_program = gradient_x_program.handle; state.Apply(); - const FormatTuple tuple = GetFormatTuple(surface.pixel_format); - glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(surface.stride)); - glActiveTexture(GL_TEXTURE0); - glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0, - tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]); - glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_RECTANGLE, LUMAD.tex.handle); glActiveTexture(GL_TEXTURE2); @@ -124,14 +118,17 @@ void Anime4kUltrafast::scale(CachedSurface& surface, const Common::Rectangle(dst_rect.left), static_cast(dst_rect.bottom), + static_cast(dst_rect.GetWidth()), + static_cast(dst_rect.GetHeight())}; + state.draw.draw_framebuffer = draw_fb_handle; state.draw.shader_program = refine_program.handle; state.Apply(); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - cur_state.texture_units[0].texture_2d, 0); + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); cur_state.Apply(); } diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h index 79a058fd5..9e89da816 100644 --- a/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h +++ b/src/video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h @@ -6,29 +6,25 @@ #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" namespace OpenGL { -class Anime4kUltrafast : public TextureFilterInterface { +class Anime4kUltrafast : public TextureFilterBase { public: - static TextureFilterInfo GetInfo() { - TextureFilterInfo info; - info.name = "Anime4K Ultrafast"; - info.clamp_scale = {2, 2}; - info.constructor = std::make_unique; - return info; - } + static constexpr std::string_view NAME = "Anime4K Ultrafast"; - Anime4kUltrafast(u16 scale_factor); - void scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) override; + explicit Anime4kUltrafast(u16 scale_factor); + void Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) override; private: + static constexpr u8 internal_scale_factor = 2; + OpenGLState state{}; OGLVertexArray vao; - OGLFramebuffer out_fbo; struct TempTex { OGLTexture tex; diff --git a/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag b/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag index 26f2526ba..4417b96f6 100644 --- a/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag +++ b/src/video_core/renderer_opengl/texture_filters/anime4k/refine.frag @@ -8,6 +8,8 @@ uniform sampler2D HOOKED; uniform sampler2DRect LUMAD; uniform sampler2DRect LUMAG; +uniform float final_scale; + const float LINE_DETECT_THRESHOLD = 0.4; const float STRENGTH = 0.6; @@ -24,7 +26,7 @@ vec4 getAverage(vec4 cc, vec4 a, vec4 b, vec4 c) { #define GetRGBAL(offset) \ RGBAL(textureOffset(HOOKED, tex_coord, offset), \ - texture(LUMAD, clamp(gl_FragCoord.xy + offset, vec2(0.0), input_max)).x) + texture(LUMAD, clamp((gl_FragCoord.xy + offset) * final_scale, vec2(0.0), input_max)).x) float min3v(float a, float b, float c) { return min(min(a, b), c); diff --git a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp index 6c0bc1f82..ce039c211 100644 --- a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp +++ b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.cpp @@ -10,45 +10,36 @@ namespace OpenGL { -Bicubic::Bicubic(u16 scale_factor) : TextureFilterInterface(scale_factor) { +Bicubic::Bicubic(u16 scale_factor) : TextureFilterBase(scale_factor) { program.Create(tex_coord_vert.data(), bicubic_frag.data()); vao.Create(); - draw_fbo.Create(); src_sampler.Create(); state.draw.shader_program = program.handle; state.draw.vertex_array = vao.handle; state.draw.shader_program = program.handle; - state.draw.draw_framebuffer = draw_fbo.handle; state.texture_units[0].sampler = src_sampler.handle; glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); -} +} // namespace OpenGL -void Bicubic::scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) { +void Bicubic::Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) { const OpenGLState cur_state = OpenGLState::GetCurState(); - - OGLTexture src_tex; - src_tex.Create(); - state.texture_units[0].texture_2d = src_tex.handle; - - state.viewport = RectToViewport(rect); + state.texture_units[0].texture_2d = src_tex; + state.draw.draw_framebuffer = draw_fb_handle; + state.viewport = {static_cast(dst_rect.left), static_cast(dst_rect.bottom), + static_cast(dst_rect.GetWidth()), + static_cast(dst_rect.GetHeight())}; state.Apply(); - const FormatTuple tuple = GetFormatTuple(surface.pixel_format); - glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(surface.stride)); - glActiveTexture(GL_TEXTURE0); - glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0, - tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]); - - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - cur_state.texture_units[0].texture_2d, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); cur_state.Apply(); } diff --git a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h index a16bdafcf..b982cc973 100644 --- a/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h +++ b/src/video_core/renderer_opengl/texture_filters/bicubic/bicubic.h @@ -6,27 +6,24 @@ #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" namespace OpenGL { -class Bicubic : public TextureFilterInterface { -public: - static TextureFilterInfo GetInfo() { - TextureFilterInfo info; - info.name = "Bicubic"; - info.constructor = std::make_unique; - return info; - } - Bicubic(u16 scale_factor); - void scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) override; +class Bicubic : public TextureFilterBase { +public: + static constexpr std::string_view NAME = "Bicubic"; + + explicit Bicubic(u16 scale_factor); + void Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) override; private: OpenGLState state{}; OGLProgram program{}; OGLVertexArray vao{}; - OGLFramebuffer draw_fbo{}; OGLSampler src_sampler{}; }; + } // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_base.h b/src/video_core/renderer_opengl/texture_filters/texture_filter_base.h new file mode 100644 index 000000000..126fdb108 --- /dev/null +++ b/src/video_core/renderer_opengl/texture_filters/texture_filter_base.h @@ -0,0 +1,26 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/common_types.h" +#include "common/math_util.h" +#include "video_core/renderer_opengl/gl_surface_params.h" + +namespace OpenGL { + +class TextureFilterBase { + friend class TextureFilterer; + virtual void Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) = 0; + +public: + explicit TextureFilterBase(u16 scale_factor) : scale_factor{scale_factor} {}; + virtual ~TextureFilterBase() = default; + + const u16 scale_factor{}; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h b/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h deleted file mode 100644 index c2bf2a686..000000000 --- a/src/video_core/renderer_opengl/texture_filters/texture_filter_interface.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include "common/common_types.h" -#include "common/math_util.h" - -namespace OpenGL { - -struct CachedSurface; -struct Viewport; - -class TextureFilterInterface { -public: - const u16 scale_factor{}; - TextureFilterInterface(u16 scale_factor) : scale_factor{scale_factor} {} - virtual void scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) = 0; - virtual ~TextureFilterInterface() = default; - -protected: - Viewport RectToViewport(const Common::Rectangle& rect); -}; - -// every texture filter should have a static GetInfo function -struct TextureFilterInfo { - std::string_view name; - struct { - u16 min, max; - } clamp_scale{1, 10}; - std::function(u16 scale_factor)> constructor; -}; - -} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp b/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp deleted file mode 100644 index 77d07111e..000000000 --- a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include "common/logging/log.h" -#include "video_core/renderer_opengl/gl_state.h" -#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h" -#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_manager.h" -#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h" - -namespace OpenGL { - -Viewport TextureFilterInterface::RectToViewport(const Common::Rectangle& rect) { - return { - static_cast(rect.left) * scale_factor, - static_cast(rect.top) * scale_factor, - static_cast(rect.GetWidth()) * scale_factor, - static_cast(rect.GetHeight()) * scale_factor, - }; -} - -namespace { -template -std::pair FilterMapPair() { - return {T::GetInfo().name, T::GetInfo()}; -}; - -struct NoFilter { - static TextureFilterInfo GetInfo() { - TextureFilterInfo info; - info.name = TextureFilterManager::NONE; - info.clamp_scale = {1, 1}; - info.constructor = [](u16) { return nullptr; }; - return info; - } -}; -} // namespace - -const std::map& -TextureFilterManager::TextureFilterMap() { - static const std::map filter_map{ - FilterMapPair(), - FilterMapPair(), - FilterMapPair(), - FilterMapPair(), - }; - return filter_map; -} - -void TextureFilterManager::SetTextureFilter(std::string filter_name, u16 new_scale_factor) { - if (name == filter_name && scale_factor == new_scale_factor) - return; - std::lock_guard lock{mutex}; - name = std::move(filter_name); - scale_factor = new_scale_factor; - updated = true; -} - -TextureFilterInterface* TextureFilterManager::GetTextureFilter() const { - return filter.get(); -} - -bool TextureFilterManager::IsUpdated() const { - return updated; -} - -void TextureFilterManager::Reset() { - std::lock_guard lock{mutex}; - updated = false; - auto iter = TextureFilterMap().find(name); - if (iter == TextureFilterMap().end()) { - LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", name); - filter = nullptr; - return; - } - - const auto& filter_info = iter->second; - - u16 clamped_scale = - std::clamp(scale_factor, filter_info.clamp_scale.min, filter_info.clamp_scale.max); - if (clamped_scale != scale_factor) - LOG_ERROR(Render_OpenGL, "Invalid scale factor {} for texture filter {}, clamped to {}", - scale_factor, filter_info.name, clamped_scale); - - filter = filter_info.constructor(clamped_scale); -} - -} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h b/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h deleted file mode 100644 index 1299a9151..000000000 --- a/src/video_core/renderer_opengl/texture_filters/texture_filter_manager.h +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h" - -namespace OpenGL { - -class TextureFilterManager { -public: - static constexpr std::string_view NONE = "none"; - struct FilterNameComp { - bool operator()(const std::string_view a, const std::string_view b) const { - bool na = a == NONE; - bool nb = b == NONE; - if (na | nb) - return na & !nb; - return a < b; - } - }; - // function ensures map is initialized before use - static const std::map& TextureFilterMap(); - - static TextureFilterManager& GetInstance() { - static TextureFilterManager singleton; - return singleton; - } - - void Destroy() { - filter.reset(); - } - void SetTextureFilter(std::string filter_name, u16 new_scale_factor); - TextureFilterInterface* GetTextureFilter() const; - // returns true if filter has been changed and a cache reset is needed - bool IsUpdated() const; - void Reset(); - -private: - std::atomic updated{false}; - std::mutex mutex; - std::string name{"none"}; - u16 scale_factor{1}; - - std::unique_ptr filter; -}; - -} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filterer.cpp b/src/video_core/renderer_opengl/texture_filters/texture_filterer.cpp new file mode 100644 index 000000000..0afa6b876 --- /dev/null +++ b/src/video_core/renderer_opengl/texture_filters/texture_filterer.cpp @@ -0,0 +1,86 @@ +/// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include "common/logging/log.h" +#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h" +#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" +#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h" + +namespace OpenGL { + +namespace { + +using TextureFilterContructor = std::function(u16)>; + +template +std::pair FilterMapPair() { + return {T::NAME, std::make_unique}; +}; + +static const std::unordered_map filter_map{ + {TextureFilterer::NONE, [](u16) { return nullptr; }}, + FilterMapPair(), + FilterMapPair(), + FilterMapPair(), +}; + +} // namespace + +TextureFilterer::TextureFilterer(std::string_view filter_name, u16 scale_factor) { + Reset(filter_name, scale_factor); +} + +bool TextureFilterer::Reset(std::string_view new_filter_name, u16 new_scale_factor) { + if (filter_name == new_filter_name && (IsNull() || filter->scale_factor == new_scale_factor)) + return false; + + auto iter = filter_map.find(new_filter_name); + if (iter == filter_map.end()) { + LOG_ERROR(Render_OpenGL, "Invalid texture filter: {}", new_filter_name); + filter = nullptr; + return true; + } + + filter_name = iter->first; + filter = iter->second(new_scale_factor); + return true; +} + +bool TextureFilterer::IsNull() const { + return !filter; +} + +bool TextureFilterer::Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, + SurfaceParams::SurfaceType type, GLuint read_fb_handle, + GLuint draw_fb_handle) { + // depth / stencil texture filtering is not supported for now + if (IsNull() || + (type != SurfaceParams::SurfaceType::Color && type != SurfaceParams::SurfaceType::Texture)) + return false; + filter->Filter(src_tex, src_rect, dst_tex, dst_rect, read_fb_handle, draw_fb_handle); + return true; +} + +std::vector TextureFilterer::GetFilterNames() { + std::vector ret; + std::transform(filter_map.begin(), filter_map.end(), std::back_inserter(ret), + [](auto pair) { return pair.first; }); + std::sort(ret.begin(), ret.end(), [](std::string_view lhs, std::string_view rhs) { + // sort lexicographically with none at the top + bool lhs_is_none{lhs == NONE}; + bool rhs_is_none{rhs == NONE}; + if (lhs_is_none || rhs_is_none) + return lhs_is_none && !rhs_is_none; + return lhs < rhs; + }); + return ret; +} + +} // namespace OpenGL \ No newline at end of file diff --git a/src/video_core/renderer_opengl/texture_filters/texture_filterer.h b/src/video_core/renderer_opengl/texture_filters/texture_filterer.h new file mode 100644 index 000000000..de3666356 --- /dev/null +++ b/src/video_core/renderer_opengl/texture_filters/texture_filterer.h @@ -0,0 +1,39 @@ +// Copyright 2020 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" +#include "common/math_util.h" +#include "video_core/renderer_opengl/gl_surface_params.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" + +namespace OpenGL { + +class TextureFilterer { +public: + static constexpr std::string_view NONE = "none"; + + explicit TextureFilterer(std::string_view filter_name, u16 scale_factor); + // returns true if the filter actually changed + bool Reset(std::string_view new_filter_name, u16 new_scale_factor); + // returns true if there is no active filter + bool IsNull() const; + // returns true if the texture was able to be filtered + bool Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, SurfaceParams::SurfaceType type, + GLuint read_fb_handle, GLuint draw_fb_handle); + + static std::vector GetFilterNames(); + +private: + std::string_view filter_name = NONE; + std::unique_ptr filter; +}; + +} // namespace OpenGL diff --git a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp index 5e7c34d1d..b1dcefc03 100644 --- a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp +++ b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.cpp @@ -48,12 +48,11 @@ namespace OpenGL { -XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterInterface(scale_factor) { +XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterBase(scale_factor) { const OpenGLState cur_state = OpenGLState::GetCurState(); program.Create(xbrz_freescale_vert.data(), xbrz_freescale_frag.data()); vao.Create(); - draw_fbo.Create(); src_sampler.Create(); state.draw.shader_program = program.handle; @@ -68,31 +67,24 @@ XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterInterface(scale_fa cur_state.Apply(); state.draw.vertex_array = vao.handle; state.draw.shader_program = program.handle; - state.draw.draw_framebuffer = draw_fbo.handle; state.texture_units[0].sampler = src_sampler.handle; } -void XbrzFreescale::scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) { +void XbrzFreescale::Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) { const OpenGLState cur_state = OpenGLState::GetCurState(); - OGLTexture src_tex; - src_tex.Create(); - state.texture_units[0].texture_2d = src_tex.handle; - - state.viewport = RectToViewport(rect); + state.texture_units[0].texture_2d = src_tex; + state.draw.draw_framebuffer = draw_fb_handle; + state.viewport = {static_cast(dst_rect.left), static_cast(dst_rect.bottom), + static_cast(dst_rect.GetWidth()), + static_cast(dst_rect.GetHeight())}; state.Apply(); - const FormatTuple tuple = GetFormatTuple(surface.pixel_format); - glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast(surface.stride)); - glActiveTexture(GL_TEXTURE0); - glTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, rect.GetWidth(), rect.GetHeight(), 0, - tuple.format, tuple.type, &surface.gl_buffer[buffer_offset]); - - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - cur_state.texture_units[0].texture_2d, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); cur_state.Apply(); } diff --git a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h index aad10f308..02c6d5d7e 100644 --- a/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h +++ b/src/video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h @@ -6,28 +6,23 @@ #include "video_core/renderer_opengl/gl_resource_manager.h" #include "video_core/renderer_opengl/gl_state.h" -#include "video_core/renderer_opengl/texture_filters/texture_filter_interface.h" +#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h" namespace OpenGL { -class XbrzFreescale : public TextureFilterInterface { +class XbrzFreescale : public TextureFilterBase { public: - static TextureFilterInfo GetInfo() { - TextureFilterInfo info; - info.name = "xBRZ freescale"; - info.constructor = std::make_unique; - return info; - } + static constexpr std::string_view NAME = "xBRZ freescale"; - XbrzFreescale(u16 scale_factor); - void scale(CachedSurface& surface, const Common::Rectangle& rect, - std::size_t buffer_offset) override; + explicit XbrzFreescale(u16 scale_factor); + void Filter(GLuint src_tex, const Common::Rectangle& src_rect, GLuint dst_tex, + const Common::Rectangle& dst_rect, GLuint read_fb_handle, + GLuint draw_fb_handle) override; private: OpenGLState state{}; OGLProgram program{}; OGLVertexArray vao{}; - OGLFramebuffer draw_fbo{}; OGLSampler src_sampler{}; }; } // namespace OpenGL diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp index f33ec7d57..619ea3d4c 100644 --- a/src/video_core/video_core.cpp +++ b/src/video_core/video_core.cpp @@ -28,6 +28,7 @@ std::atomic g_use_disk_shader_cache; std::atomic g_renderer_bg_color_update_requested; std::atomic g_renderer_sampler_update_requested; std::atomic g_renderer_shader_update_requested; +std::atomic g_texture_filter_update_requested; // Screenshot std::atomic g_renderer_screenshot_requested; void* g_screenshot_bits; diff --git a/src/video_core/video_core.h b/src/video_core/video_core.h index 46cf1e86d..409f4deb2 100644 --- a/src/video_core/video_core.h +++ b/src/video_core/video_core.h @@ -36,6 +36,7 @@ extern std::atomic g_use_disk_shader_cache; extern std::atomic g_renderer_bg_color_update_requested; extern std::atomic g_renderer_sampler_update_requested; extern std::atomic g_renderer_shader_update_requested; +extern std::atomic g_texture_filter_update_requested; // Screenshot extern std::atomic g_renderer_screenshot_requested; extern void* g_screenshot_bits;