diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt index 8608616c3..9aa4b5902 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/IntSetting.kt @@ -28,6 +28,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { LOCK_DRAWER("lock_drawer"), VERTICAL_ALIGNMENT("vertical_alignment"), FSR_SHARPENING_SLIDER("fsr_sharpening_slider"), + FSR2_QUALITY_MODE("fsr2_quality_mode"), // Zep Zone settings MEMORY_LAYOUT_MODE("memory_layout_mode"), diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt index 922d63dd3..8a5765166 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/model/view/SettingsItem.kt @@ -245,6 +245,15 @@ abstract class SettingsItem( units = "%" ) ) + put( + SingleChoiceSetting( + IntSetting.FSR2_QUALITY_MODE, + titleId = R.string.fsr2_quality_mode, + descriptionId = R.string.fsr2_quality_mode_description, + choicesId = R.array.fsr2QualityModeNames, + valuesId = R.array.fsr2QualityModeValues + ) + ) put( SingleChoiceSetting( IntSetting.RENDERER_ANTI_ALIASING, diff --git a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt index 6b7919c34..3f8bd9e09 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -182,6 +182,7 @@ class SettingsFragmentPresenter( add(IntSetting.RENDERER_VSYNC.key) add(IntSetting.RENDERER_SCALING_FILTER.key) add(IntSetting.FSR_SHARPENING_SLIDER.key) + add(IntSetting.FSR2_QUALITY_MODE.key) add(IntSetting.RENDERER_ANTI_ALIASING.key) add(IntSetting.MAX_ANISOTROPY.key) add(IntSetting.RENDERER_SCREEN_LAYOUT.key) diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index b8950180c..537d85f59 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -228,6 +228,12 @@ Filtre de fenêtre adaptatif Netteté FSR Détermine à quel point l\'image sera affinée lors de l\'utilisation du contraste dynamique FSR. + Mode qualité FSR 2.0 + Sélectionne le mode qualité pour l\'upscaling FSR 2.0. Qualité offre une meilleure qualité d\'image, Performance offre de meilleures performances. + Qualité + Équilibré + Performance + Performance Ultra Méthode d\'anticrénelage Forcer les fréquences maximales (Adreno uniquement) Forcer le GPU à fonctionner à ses fréquences maximales possibles (les contraintes thermiques seront toujours appliquées). @@ -558,6 +564,7 @@ Gaussien ScaleForce AMD FidelityFX™ Super Resolution + AMD FidelityFX™ Super Resolution 2.0 Aucune diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index cf9c29058..62d98fcbc 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -160,6 +160,7 @@ @string/scaling_filter_gaussian @string/scaling_filter_scale_force @string/scaling_filter_fsr + @string/scaling_filter_fsr2 @@ -169,6 +170,21 @@ 3 4 5 + 6 + + + + @string/fsr2_quality_quality + @string/fsr2_quality_balanced + @string/fsr2_quality_performance + @string/fsr2_quality_ultra_performance + + + + 0 + 1 + 2 + 3 diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 38c219001..df19d9491 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -229,6 +229,12 @@ Window adapting filter FSR sharpness Determines how sharpened the image will look while using FSR\'s dynamic contrast + FSR 2.0 Quality Mode + Selects the quality mode for FSR 2.0 upscaling. Quality provides better image quality, Performance provides better performance. + Quality + Balanced + Performance + Ultra Performance Anti-aliasing method Force maximum clocks (Adreno only) Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied). @@ -587,6 +593,7 @@ Gaussian ScaleForce AMD FidelityFX™ Super Resolution + AMD FidelityFX™ Super Resolution 2.0 None diff --git a/src/citron/configuration/configure_graphics.cpp b/src/citron/configuration/configure_graphics.cpp index 0c814b043..f3910ebaf 100644 --- a/src/citron/configuration/configure_graphics.cpp +++ b/src/citron/configuration/configure_graphics.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -288,6 +289,11 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) { continue; } + // Store reference to the FSR sharpness widget for later use + if (setting->Id() == Settings::values.fsr_sharpening_slider.Id()) { + fsr_sharpness_widget = widget; + } + if (setting->Id() == Settings::values.renderer_backend.Id()) { // Add the renderer combobox now so it's at the top api_grid_layout->addWidget(widget); @@ -366,6 +372,41 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) { api_grid_layout->addWidget(widget); } + // Set up FSR sharpness slider conditional enabling + if (fsr_sharpness_widget) { + // Create a function to update the enabled state based on scaling filter + auto update_fsr_sharpness_enabled = [this]() { + if (fsr_sharpness_widget) { + const auto scaling_filter = Settings::values.scaling_filter.GetValue(); + const bool fsr2_selected = (scaling_filter == Settings::ScalingFilter::Fsr2); + fsr_sharpness_widget->setEnabled(!fsr2_selected); + + // Grey out the widget when disabled + if (fsr2_selected) { + fsr_sharpness_widget->setStyleSheet(QStringLiteral("QWidget { color: gray; }")); + } else { + fsr_sharpness_widget->setStyleSheet(QStringLiteral("")); + } + } + }; + + // Initial state + update_fsr_sharpness_enabled(); + + // Connect to scaling filter changes + if (!Settings::IsConfiguringGlobal()) { + // Find the scaling filter widget and connect to its changes + for (const auto& [id, widget] : hold_graphics) { + if (id == Settings::values.scaling_filter.Id()) { + auto* config_widget = static_cast(widget); + QObject::connect(config_widget->combobox, QOverload::of(&QComboBox::activated), + [update_fsr_sharpness_enabled]() { update_fsr_sharpness_enabled(); }); + break; + } + } + } + } + // Background color is too specific to build into the new system, so we manage it here // (3 settings, all collected into a single widget with a QColor to manage on top) if (Settings::IsConfiguringGlobal()) { diff --git a/src/citron/configuration/configure_graphics.h b/src/citron/configuration/configure_graphics.h index eae4a627a..33ae92031 100644 --- a/src/citron/configuration/configure_graphics.h +++ b/src/citron/configuration/configure_graphics.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -113,4 +114,5 @@ private: QWidget* shader_backend_widget; QComboBox* aspect_ratio_combobox; QComboBox* resolution_combobox; + QWidget* fsr_sharpness_widget; }; diff --git a/src/citron/configuration/shared_translation.cpp b/src/citron/configuration/shared_translation.cpp index 120e0a72a..aa9e2482d 100644 --- a/src/citron/configuration/shared_translation.cpp +++ b/src/citron/configuration/shared_translation.cpp @@ -127,6 +127,8 @@ std::unique_ptr InitializeTranslations(QWidget* parent) { INSERT(Settings, scaling_filter, tr("Window Adapting Filter:"), QStringLiteral()); INSERT(Settings, fsr_sharpening_slider, tr("FSR Sharpness:"), tr("Determines how sharpened the image will look while using FSR's dynamic contrast.")); + INSERT(Settings, fsr2_quality_mode, tr("FSR 2.0 Quality Mode:"), + tr("Selects the quality mode for FSR 2.0 upscaling. Quality provides better image quality, Performance provides better performance.")); INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"), tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA has a " "lower performance impact and can produce a better and more stable picture under " @@ -394,6 +396,7 @@ std::unique_ptr ComboboxEnumeration(QWidget* parent) { PAIR(ScalingFilter, Gaussian, tr("Gaussian")), PAIR(ScalingFilter, ScaleForce, tr("ScaleForce")), PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX™️ Super Resolution")), + PAIR(ScalingFilter, Fsr2, tr("AMD FidelityFX™️ Super Resolution 2.0")), }}); translations->insert({Settings::EnumMetadata::Index(), { @@ -401,6 +404,13 @@ std::unique_ptr ComboboxEnumeration(QWidget* parent) { PAIR(AntiAliasing, Fxaa, tr("FXAA")), PAIR(AntiAliasing, Smaa, tr("SMAA")), }}); + translations->insert({Settings::EnumMetadata::Index(), + { + PAIR(FSR2QualityMode, Quality, tr("Quality")), + PAIR(FSR2QualityMode, Balanced, tr("Balanced")), + PAIR(FSR2QualityMode, Performance, tr("Performance")), + PAIR(FSR2QualityMode, UltraPerformance, tr("Ultra Performance")), + }}); translations->insert({Settings::EnumMetadata::Index(), { PAIR(AspectRatio, R16_9, tr("Default (16:9)")), diff --git a/src/citron/configuration/shared_translation.h b/src/citron/configuration/shared_translation.h index d5fc3b8de..6664e209a 100644 --- a/src/citron/configuration/shared_translation.h +++ b/src/citron/configuration/shared_translation.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -40,6 +41,7 @@ static const std::map scaling_filter_texts_map {Settings::ScalingFilter::ScaleForce, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "ScaleForce"))}, {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FSR"))}, + {Settings::ScalingFilter::Fsr2, QStringLiteral(QT_TRANSLATE_NOOP("GMainWindow", "FSR 2.0"))}, }; static const std::map use_docked_mode_texts_map = { diff --git a/src/common/settings.h b/src/common/settings.h index a7ce9e5e8..dd82c4994 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -348,6 +348,16 @@ struct Values { true, true}; + SwitchableSetting fsr2_quality_mode{linkage, + FSR2QualityMode::Performance, // Performance by default + FSR2QualityMode::Quality, + FSR2QualityMode::UltraPerformance, + "fsr2_quality_mode", + Category::Renderer, + Specialization::Default, + true, + true}; + SwitchableSetting bg_red{ linkage, 0, "bg_red", Category::Renderer, Specialization::Default, true, true}; SwitchableSetting bg_green{ diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 7afbf41ac..6151e7619 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -147,10 +147,12 @@ ENUM(NvdecEmulation, Off, Cpu, Gpu); ENUM(ResolutionSetup, Res1_2X, Res3_4X, Res1X, Res3_2X, Res2X, Res3X, Res4X, Res5X, Res6X, Res7X, Res8X); -ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, ScaleForce, Fsr, MaxEnum); +ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, ScaleForce, Fsr, Fsr2, MaxEnum); ENUM(AntiAliasing, None, Fxaa, Smaa, MaxEnum); +ENUM(FSR2QualityMode, Quality, Balanced, Performance, UltraPerformance); + ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, R32_9, Stretch); ENUM(ConsoleMode, Handheld, Docked); diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 28b773688..0f6d0239a 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -123,6 +123,8 @@ add_library(video_core STATIC renderer_opengl/present/filters.h renderer_opengl/present/fsr.cpp renderer_opengl/present/fsr.h + renderer_opengl/present/fsr2.cpp + renderer_opengl/present/fsr2.h renderer_opengl/present/fxaa.cpp renderer_opengl/present/fxaa.h renderer_opengl/present/layer.cpp @@ -178,6 +180,8 @@ add_library(video_core STATIC renderer_vulkan/present/filters.h renderer_vulkan/present/fsr.cpp renderer_vulkan/present/fsr.h + renderer_vulkan/present/fsr2.cpp + renderer_vulkan/present/fsr2.h renderer_vulkan/present/fxaa.cpp renderer_vulkan/present/fxaa.h renderer_vulkan/present/layer.cpp diff --git a/src/video_core/renderer_opengl/gl_blit_screen.cpp b/src/video_core/renderer_opengl/gl_blit_screen.cpp index 9260a4dc4..091b09ef3 100644 --- a/src/video_core/renderer_opengl/gl_blit_screen.cpp +++ b/src/video_core/renderer_opengl/gl_blit_screen.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/settings.h" @@ -87,6 +88,7 @@ void BlitScreen::CreateWindowAdapt() { window_adapt = MakeScaleForce(device); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Fsr2: case Settings::ScalingFilter::Bilinear: default: window_adapt = MakeBilinear(device); diff --git a/src/video_core/renderer_opengl/present/fsr2.cpp b/src/video_core/renderer_opengl/present/fsr2.cpp new file mode 100644 index 000000000..572ab6595 --- /dev/null +++ b/src/video_core/renderer_opengl/present/fsr2.cpp @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/common_types.h" +#include "common/settings.h" +#include "video_core/fsr.h" +#include "video_core/host_shaders/ffx_a_h.h" +#include "video_core/host_shaders/ffx_fsr1_h.h" +#include "video_core/host_shaders/full_screen_triangle_vert.h" +#include "video_core/host_shaders/opengl_fidelityfx_fsr_easu_frag.h" +#include "video_core/host_shaders/opengl_fidelityfx_fsr_frag.h" +#include "video_core/host_shaders/opengl_fidelityfx_fsr_rcas_frag.h" +#include "video_core/renderer_opengl/gl_shader_manager.h" +#include "video_core/renderer_opengl/gl_shader_util.h" +#include "video_core/renderer_opengl/present/fsr2.h" +#include "video_core/renderer_opengl/present/util.h" + +namespace OpenGL { +using namespace FSR; + +using FsrConstants = std::array; + +FSR2::FSR2(u32 output_width_, u32 output_height_) : width(output_width_), height(output_height_) { + std::string fsr_source{HostShaders::OPENGL_FIDELITYFX_FSR_FRAG}; + ReplaceInclude(fsr_source, "ffx_a.h", HostShaders::FFX_A_H); + ReplaceInclude(fsr_source, "ffx_fsr1.h", HostShaders::FFX_FSR1_H); + + std::string fsr_easu_source{HostShaders::OPENGL_FIDELITYFX_FSR_EASU_FRAG}; + std::string fsr_rcas_source{HostShaders::OPENGL_FIDELITYFX_FSR_RCAS_FRAG}; + ReplaceInclude(fsr_easu_source, "opengl_fidelityfx_fsr.frag", fsr_source); + ReplaceInclude(fsr_rcas_source, "opengl_fidelityfx_fsr.frag", fsr_source); + + vert = CreateProgram(HostShaders::FULL_SCREEN_TRIANGLE_VERT, GL_VERTEX_SHADER); + easu_frag = CreateProgram(fsr_easu_source, GL_FRAGMENT_SHADER); + rcas_frag = CreateProgram(fsr_rcas_source, GL_FRAGMENT_SHADER); + + glProgramUniform2f(vert.handle, 0, 1.0f, -1.0f); + glProgramUniform2f(vert.handle, 1, 0.0f, 1.0f); + + sampler = CreateBilinearSampler(); + framebuffer.Create(); + + easu_tex.Create(GL_TEXTURE_2D); + glTextureStorage2D(easu_tex.handle, 1, GL_RGBA16F, width, height); + + rcas_tex.Create(GL_TEXTURE_2D); + glTextureStorage2D(rcas_tex.handle, 1, GL_RGBA16F, width, height); +} + +FSR2::~FSR2() = default; + +GLuint FSR2::Draw(ProgramManager& program_manager, GLuint texture, u32 input_image_width, + u32 input_image_height, const Common::Rectangle& crop_rect) { + const f32 input_width = static_cast(input_image_width); + const f32 input_height = static_cast(input_image_height); + const f32 output_width = static_cast(width); + const f32 output_height = static_cast(height); + const f32 viewport_width = (crop_rect.right - crop_rect.left) * input_width; + const f32 viewport_x = crop_rect.left * input_width; + const f32 viewport_height = (crop_rect.bottom - crop_rect.top) * input_height; + const f32 viewport_y = crop_rect.top * input_height; + + FsrConstants easu_con{}; + FsrConstants rcas_con{}; + + FsrEasuConOffset(easu_con.data() + 0, easu_con.data() + 4, easu_con.data() + 8, + easu_con.data() + 12, viewport_width, viewport_height, input_width, + input_height, output_width, output_height, viewport_x, viewport_y); + + // FSR 2.0 uses a different sharpening calculation based on quality mode + const auto quality_mode = Settings::values.fsr2_quality_mode.GetValue(); + const float sharpening = [quality_mode]() { + switch (quality_mode) { + case Settings::FSR2QualityMode::Quality: + return 0.2f; + case Settings::FSR2QualityMode::Balanced: + return 0.4f; + case Settings::FSR2QualityMode::Performance: + return 0.6f; + case Settings::FSR2QualityMode::UltraPerformance: + return 0.8f; + default: + return 0.4f; + } + }(); + + FsrRcasCon(rcas_con.data(), sharpening); + + glProgramUniform4uiv(easu_frag.handle, 0, sizeof(easu_con), easu_con.data()); + glProgramUniform4uiv(rcas_frag.handle, 0, sizeof(rcas_con), rcas_con.data()); + + glFrontFace(GL_CW); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer.handle); + glNamedFramebufferTexture(framebuffer.handle, GL_COLOR_ATTACHMENT0, easu_tex.handle, 0); + glViewportIndexedf(0, 0.0f, 0.0f, output_width, output_height); + program_manager.BindPresentPrograms(vert.handle, easu_frag.handle); + glBindTextureUnit(0, texture); + glBindSampler(0, sampler.handle); + glDrawArrays(GL_TRIANGLES, 0, 3); + + glNamedFramebufferTexture(framebuffer.handle, GL_COLOR_ATTACHMENT0, rcas_tex.handle, 0); + program_manager.BindPresentPrograms(vert.handle, rcas_frag.handle); + glDrawArrays(GL_TRIANGLES, 0, 3); + + glFrontFace(GL_CCW); + return rcas_tex.handle; +} + +} // namespace OpenGL \ No newline at end of file diff --git a/src/video_core/renderer_opengl/present/fsr2.h b/src/video_core/renderer_opengl/present/fsr2.h new file mode 100644 index 000000000..9284d5af2 --- /dev/null +++ b/src/video_core/renderer_opengl/present/fsr2.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/common_types.h" +#include "common/math_util.h" +#include "video_core/renderer_opengl/gl_resource_manager.h" + +namespace OpenGL { + +class ProgramManager; + +class FSR2 { +public: + explicit FSR2(u32 output_width_, u32 output_height_); + ~FSR2(); + + GLuint Draw(ProgramManager& program_manager, GLuint texture, u32 input_image_width, + u32 input_image_height, const Common::Rectangle& crop_rect); + + bool NeedsRecreation(const Common::Rectangle& screen) const { + return screen.GetWidth() != width || screen.GetHeight() != height; + } + +private: + const u32 width; + const u32 height; + + OGLProgram vert; + OGLProgram easu_frag; + OGLProgram rcas_frag; + OGLSampler sampler; + OGLFramebuffer framebuffer; + OGLTexture easu_tex; + OGLTexture rcas_tex; +}; + +} // namespace OpenGL \ No newline at end of file diff --git a/src/video_core/renderer_opengl/present/layer.cpp b/src/video_core/renderer_opengl/present/layer.cpp index a0f865937..0bdec4cb5 100644 --- a/src/video_core/renderer_opengl/present/layer.cpp +++ b/src/video_core/renderer_opengl/present/layer.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "video_core/framebuffer_config.h" @@ -6,6 +7,7 @@ #include "video_core/renderer_opengl/gl_blit_screen.h" #include "video_core/renderer_opengl/gl_rasterizer.h" #include "video_core/renderer_opengl/present/fsr.h" +#include "video_core/renderer_opengl/present/fsr2.h" #include "video_core/renderer_opengl/present/fxaa.h" #include "video_core/renderer_opengl/present/layer.h" #include "video_core/renderer_opengl/present/present_uniforms.h" @@ -74,6 +76,14 @@ GLuint Layer::ConfigureDraw(std::array& out_matrix, texture = fsr->Draw(program_manager, texture, info.scaled_width, info.scaled_height, crop); crop = {0, 0, 1, 1}; } + if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr2) { + if (!fsr2 || fsr2->NeedsRecreation(layout.screen)) { + fsr2 = std::make_unique(layout.screen.GetWidth(), layout.screen.GetHeight()); + } + + texture = fsr2->Draw(program_manager, texture, info.scaled_width, info.scaled_height, crop); + crop = {0, 0, 1, 1}; + } out_matrix = MakeOrthographicMatrix(static_cast(layout.width), static_cast(layout.height)); diff --git a/src/video_core/renderer_opengl/present/layer.h b/src/video_core/renderer_opengl/present/layer.h index 5b15b730f..0cfd6df99 100644 --- a/src/video_core/renderer_opengl/present/layer.h +++ b/src/video_core/renderer_opengl/present/layer.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -27,6 +28,7 @@ namespace OpenGL { struct FramebufferTextureInfo; class FSR; +class FSR2; class FXAA; class ProgramManager; class RasterizerOpenGL; @@ -77,6 +79,7 @@ private: TextureInfo framebuffer_texture; std::unique_ptr fsr; + std::unique_ptr fsr2; std::unique_ptr fxaa; std::unique_ptr smaa; }; diff --git a/src/video_core/renderer_vulkan/present/fsr2.cpp b/src/video_core/renderer_vulkan/present/fsr2.cpp new file mode 100644 index 000000000..714f12942 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr2.cpp @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/common_types.h" +#include "common/div_ceil.h" +#include "common/settings.h" + +#include "video_core/fsr.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp16_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp32_frag_spv.h" +#include "video_core/host_shaders/vulkan_fidelityfx_fsr_vert_spv.h" +#include "video_core/renderer_vulkan/present/fsr2.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { +using namespace FSR; + +using PushConstants = std::array; + +FSR2::FSR2(const Device& device, MemoryAllocator& memory_allocator, size_t image_count, + VkExtent2D extent) + : m_device{device}, m_memory_allocator{memory_allocator}, + m_image_count{image_count}, m_extent{extent} { + + CreateImages(); + CreateRenderPasses(); + CreateSampler(); + CreateShaders(); + CreateDescriptorPool(); + CreateDescriptorSetLayout(); + CreateDescriptorSets(); + CreatePipelineLayouts(); + CreatePipelines(); +} + +void FSR2::CreateImages() { + m_dynamic_images.resize(m_image_count); + for (auto& images : m_dynamic_images) { + images.images[Easu] = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.images[Rcas] = + CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_views[Easu] = + CreateWrappedImageView(m_device, images.images[Easu], VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_views[Rcas] = + CreateWrappedImageView(m_device, images.images[Rcas], VK_FORMAT_R16G16B16A16_SFLOAT); + } +} + +void FSR2::CreateRenderPasses() { + m_renderpass = CreateWrappedRenderPass(m_device, VK_FORMAT_R16G16B16A16_SFLOAT); + + for (auto& images : m_dynamic_images) { + images.framebuffers[Easu] = + CreateWrappedFramebuffer(m_device, m_renderpass, images.image_views[Easu], m_extent); + images.framebuffers[Rcas] = + CreateWrappedFramebuffer(m_device, m_renderpass, images.image_views[Rcas], m_extent); + } +} + +void FSR2::CreateSampler() { + m_sampler = CreateBilinearSampler(m_device); +} + +void FSR2::CreateShaders() { + m_vert_shader = BuildShader(m_device, VULKAN_FIDELITYFX_FSR_VERT_SPV); + + if (m_device.IsFloat16Supported()) { + m_easu_shader = BuildShader(m_device, VULKAN_FIDELITYFX_FSR_EASU_FP16_FRAG_SPV); + m_rcas_shader = BuildShader(m_device, VULKAN_FIDELITYFX_FSR_RCAS_FP16_FRAG_SPV); + } else { + m_easu_shader = BuildShader(m_device, VULKAN_FIDELITYFX_FSR_EASU_FP32_FRAG_SPV); + m_rcas_shader = BuildShader(m_device, VULKAN_FIDELITYFX_FSR_RCAS_FP32_FRAG_SPV); + } +} + +void FSR2::CreateDescriptorPool() { + // EASU: 1 descriptor + // RCAS: 1 descriptor + // 2 descriptors, 2 descriptor sets per invocation + m_descriptor_pool = CreateWrappedDescriptorPool(m_device, 2 * m_image_count, 2 * m_image_count); +} + +void FSR2::CreateDescriptorSetLayout() { + m_descriptor_set_layout = + CreateWrappedDescriptorSetLayout(m_device, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); +} + +void FSR2::CreateDescriptorSets() { + std::vector layouts(MaxFsr2Stage, *m_descriptor_set_layout); + + for (auto& images : m_dynamic_images) { + images.descriptor_sets = CreateWrappedDescriptorSets(m_descriptor_pool, layouts); + } +} + +void FSR2::CreatePipelineLayouts() { + const VkPushConstantRange range{ + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = sizeof(PushConstants), + }; + VkPipelineLayoutCreateInfo ci{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = 1, + .pSetLayouts = m_descriptor_set_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &range, + }; + + m_pipeline_layout = m_device.GetLogical().CreatePipelineLayout(ci); +} + +void FSR2::CreatePipelines() { + m_easu_pipeline = CreateWrappedPipeline(m_device, m_renderpass, m_pipeline_layout, + std::tie(m_vert_shader, m_easu_shader)); + m_rcas_pipeline = CreateWrappedPipeline(m_device, m_renderpass, m_pipeline_layout, + std::tie(m_vert_shader, m_rcas_shader)); +} + +void FSR2::UpdateDescriptorSets(VkImageView image_view, size_t image_index) { + Images& images = m_dynamic_images[image_index]; + std::vector image_infos; + std::vector updates; + image_infos.reserve(2); + + updates.push_back(CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, + images.descriptor_sets[Easu], 0)); + updates.push_back(CreateWriteDescriptorSet(image_infos, *m_sampler, *images.image_views[Easu], + images.descriptor_sets[Rcas], 0)); + + m_device.GetLogical().UpdateDescriptorSets(updates, {}); +} + +void FSR2::UploadImages(Scheduler& scheduler) { + if (m_images_ready) { + return; + } + + scheduler.Record([&](vk::CommandBuffer cmdbuf) { + for (auto& image : m_dynamic_images) { + ClearColorImage(cmdbuf, *image.images[Easu]); + ClearColorImage(cmdbuf, *image.images[Rcas]); + } + }); + scheduler.Finish(); + + m_images_ready = true; +} + +VkImageView FSR2::Draw(Scheduler& scheduler, size_t image_index, VkImage source_image, + VkImageView source_image_view, VkExtent2D input_image_extent, + const Common::Rectangle& crop_rect) { + Images& images = m_dynamic_images[image_index]; + + VkImage easu_image = *images.images[Easu]; + VkImage rcas_image = *images.images[Rcas]; + VkDescriptorSet easu_descriptor_set = images.descriptor_sets[Easu]; + VkDescriptorSet rcas_descriptor_set = images.descriptor_sets[Rcas]; + VkFramebuffer easu_framebuffer = *images.framebuffers[Easu]; + VkFramebuffer rcas_framebuffer = *images.framebuffers[Rcas]; + VkPipeline easu_pipeline = *m_easu_pipeline; + VkPipeline rcas_pipeline = *m_rcas_pipeline; + VkPipelineLayout pipeline_layout = *m_pipeline_layout; + VkRenderPass renderpass = *m_renderpass; + VkExtent2D extent = m_extent; + + const f32 input_image_width = static_cast(input_image_extent.width); + const f32 input_image_height = static_cast(input_image_extent.height); + const f32 output_image_width = static_cast(extent.width); + const f32 output_image_height = static_cast(extent.height); + const f32 viewport_width = (crop_rect.right - crop_rect.left) * input_image_width; + const f32 viewport_x = crop_rect.left * input_image_width; + const f32 viewport_height = (crop_rect.bottom - crop_rect.top) * input_image_height; + const f32 viewport_y = crop_rect.top * input_image_height; + + PushConstants easu_con{}; + PushConstants rcas_con{}; + FsrEasuConOffset(easu_con.data() + 0, easu_con.data() + 4, easu_con.data() + 8, + easu_con.data() + 12, viewport_width, viewport_height, input_image_width, + input_image_height, output_image_width, output_image_height, viewport_x, + viewport_y); + + // FSR 2.0 uses a different sharpening calculation based on quality mode + const auto quality_mode = Settings::values.fsr2_quality_mode.GetValue(); + const float sharpening = [quality_mode]() { + switch (quality_mode) { + case Settings::FSR2QualityMode::Quality: + return 0.2f; + case Settings::FSR2QualityMode::Balanced: + return 0.4f; + case Settings::FSR2QualityMode::Performance: + return 0.6f; + case Settings::FSR2QualityMode::UltraPerformance: + return 0.8f; + default: + return 0.4f; + } + }(); + FsrRcasCon(rcas_con.data(), sharpening); + + UploadImages(scheduler); + UpdateDescriptorSets(source_image_view, image_index); + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([=](vk::CommandBuffer cmdbuf) { + TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, easu_framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, easu_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, + easu_descriptor_set, {}); + cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, easu_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + + TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, rcas_framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, rcas_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, + rcas_descriptor_set, {}); + cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, rcas_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + + TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); + }); + + return *images.image_views[Rcas]; +} + +} // namespace Vulkan \ No newline at end of file diff --git a/src/video_core/renderer_vulkan/present/fsr2.h b/src/video_core/renderer_vulkan/present/fsr2.h new file mode 100644 index 000000000..8a68eb329 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/fsr2.h @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/math_util.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class Scheduler; + +class FSR2 { +public: + explicit FSR2(const Device& device, MemoryAllocator& memory_allocator, size_t image_count, + VkExtent2D extent); + VkImageView Draw(Scheduler& scheduler, size_t image_index, VkImage source_image, + VkImageView source_image_view, VkExtent2D input_image_extent, + const Common::Rectangle& crop_rect); + +private: + void CreateImages(); + void CreateRenderPasses(); + void CreateSampler(); + void CreateShaders(); + void CreateDescriptorPool(); + void CreateDescriptorSetLayout(); + void CreateDescriptorSets(); + void CreatePipelineLayouts(); + void CreatePipelines(); + + void UploadImages(Scheduler& scheduler); + void UpdateDescriptorSets(VkImageView image_view, size_t image_index); + + const Device& m_device; + MemoryAllocator& m_memory_allocator; + const size_t m_image_count; + const VkExtent2D m_extent; + + enum Fsr2Stage { + Easu, + Rcas, + MaxFsr2Stage, + }; + + vk::DescriptorPool m_descriptor_pool; + vk::DescriptorSetLayout m_descriptor_set_layout; + vk::PipelineLayout m_pipeline_layout; + vk::ShaderModule m_vert_shader; + vk::ShaderModule m_easu_shader; + vk::ShaderModule m_rcas_shader; + vk::Pipeline m_easu_pipeline; + vk::Pipeline m_rcas_pipeline; + vk::RenderPass m_renderpass; + vk::Sampler m_sampler; + + struct Images { + vk::DescriptorSets descriptor_sets; + std::array images; + std::array image_views; + std::array framebuffers; + }; + std::vector m_dynamic_images; + bool m_images_ready{}; +}; + +} // namespace Vulkan \ No newline at end of file diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp index 4e41afe5b..57dbf76db 100644 --- a/src/video_core/renderer_vulkan/present/layer.cpp +++ b/src/video_core/renderer_vulkan/present/layer.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "video_core/present.h" @@ -7,6 +8,7 @@ #include "common/settings.h" #include "video_core/framebuffer_config.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/fsr2.h" #include "video_core/renderer_vulkan/present/fxaa.h" #include "video_core/renderer_vulkan/present/layer.h" #include "video_core/renderer_vulkan/present/present_push_constants.h" @@ -57,6 +59,9 @@ Layer::Layer(const Device& device_, MemoryAllocator& memory_allocator_, Schedule if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr) { CreateFSR(output_size); } + if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr2) { + CreateFSR2(output_size); + } } Layer::~Layer() { @@ -107,6 +112,11 @@ void Layer::ConfigureDraw(PresentPushConstants* out_push_constants, render_extent, crop_rect); crop_rect = {0, 0, 1, 1}; } + if (fsr2) { + source_image_view = fsr2->Draw(scheduler, image_index, source_image, source_image_view, + render_extent, crop_rect); + crop_rect = {0, 0, 1, 1}; + } SetMatrixData(*out_push_constants, layout); SetVertexData(*out_push_constants, layout, crop_rect); @@ -157,6 +167,10 @@ void Layer::CreateFSR(VkExtent2D output_size) { fsr = std::make_unique(device, memory_allocator, image_count, output_size); } +void Layer::CreateFSR2(VkExtent2D output_size) { + fsr2 = std::make_unique(device, memory_allocator, image_count, output_size); +} + void Layer::RefreshResources(const Tegra::FramebufferConfig& framebuffer) { if (framebuffer.width == raw_width && framebuffer.height == raw_height && framebuffer.pixel_format == pixel_format && !raw_images.empty()) { diff --git a/src/video_core/renderer_vulkan/present/layer.h b/src/video_core/renderer_vulkan/present/layer.h index f5effdcd7..348f1d650 100644 --- a/src/video_core/renderer_vulkan/present/layer.h +++ b/src/video_core/renderer_vulkan/present/layer.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -30,6 +31,7 @@ namespace Vulkan { class AntiAliasPass; class Device; class FSR; +class FSR2; class MemoryAllocator; struct PresentPushConstants; class RasterizerVulkan; @@ -55,6 +57,7 @@ private: void CreateStagingBuffer(const Tegra::FramebufferConfig& framebuffer); void CreateRawImages(const Tegra::FramebufferConfig& framebuffer); void CreateFSR(VkExtent2D output_size); + void CreateFSR2(VkExtent2D output_size); void RefreshResources(const Tegra::FramebufferConfig& framebuffer); void SetAntiAliasPass(); @@ -90,6 +93,7 @@ private: std::unique_ptr anti_alias{}; std::unique_ptr fsr{}; + std::unique_ptr fsr2{}; std::vector resource_ticks{}; }; diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index b7797f833..5a1300b40 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "video_core/framebuffer_config.h" @@ -44,6 +45,7 @@ void BlitScreen::SetWindowAdaptPass() { window_adapt = MakeScaleForce(device, swapchain_view_format); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Fsr2: case Settings::ScalingFilter::Bilinear: default: window_adapt = MakeBilinear(device, swapchain_view_format);