diff --git a/src/android/app/src/main/res/values-fa/strings.xml b/src/android/app/src/main/res/values-fa/strings.xml
index 39f0432d8e..8510e146b9 100644
--- a/src/android/app/src/main/res/values-fa/strings.xml
+++ b/src/android/app/src/main/res/values-fa/strings.xml
@@ -750,8 +750,6 @@
هیچ
- FXAA
- SMAA
خودکار
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index f3a2a069e7..44d35b4b84 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -266,6 +266,7 @@
- @string/anti_aliasing_none
- @string/anti_aliasing_fxaa
- @string/anti_aliasing_smaa
+ - @string/anti_aliasing_ssaa
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 77be250537..50707fe72a 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -1087,6 +1087,7 @@
None
FXAA
SMAA
+ SSAA
Auto
diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h
index da142e8e1c..f21c59be1c 100644
--- a/src/common/settings_enums.h
+++ b/src/common/settings_enums.h
@@ -146,7 +146,7 @@ ENUM(FullscreenMode, Borderless, Exclusive);
ENUM(NvdecEmulation, Off, Cpu, Gpu);
ENUM(ResolutionSetup, Res1_4X, Res1_2X, Res3_4X, Res1X, Res5_4X, Res3_2X, Res2X, Res3X, Res4X, Res5X, Res6X, Res7X, Res8X);
ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, Sgsr, SgsrEdge, MaxEnum);
-ENUM(AntiAliasing, None, Fxaa, Smaa, MaxEnum);
+ENUM(AntiAliasing, None, Fxaa, Smaa, Ssaa, MaxEnum);
ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch);
ENUM(ConsoleMode, Handheld, Docked);
ENUM(AppletMode, HLE, LLE);
diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp
index 5c63732a3e..45b08d4e5c 100644
--- a/src/qt_common/config/shared_translation.cpp
+++ b/src/qt_common/config/shared_translation.cpp
@@ -500,6 +500,7 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) {
PAIR(AntiAliasing, None, tr("None")),
PAIR(AntiAliasing, Fxaa, tr("FXAA")),
PAIR(AntiAliasing, Smaa, tr("SMAA")),
+ PAIR(AntiAliasing, Ssaa, tr("SSAA")),
}});
translations->insert({Settings::EnumMetadata::Index(),
{
diff --git a/src/qt_common/config/shared_translation.h b/src/qt_common/config/shared_translation.h
index c34b5162c4..28ca43853c 100644
--- a/src/qt_common/config/shared_translation.h
+++ b/src/qt_common/config/shared_translation.h
@@ -31,6 +31,7 @@ static const std::map anti_aliasing_texts_map =
{Settings::AntiAliasing::None, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "None"))},
{Settings::AntiAliasing::Fxaa, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FXAA"))},
{Settings::AntiAliasing::Smaa, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SMAA"))},
+ {Settings::AntiAliasing::Ssaa, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SSAA"))},
};
static const std::map scaling_filter_texts_map = {
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 53b0d1638b..67482039aa 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -125,6 +125,8 @@ add_library(video_core STATIC
renderer_vulkan/present/fsr.h
renderer_vulkan/present/fxaa.cpp
renderer_vulkan/present/fxaa.h
+ renderer_vulkan/present/ssaa.cpp
+ renderer_vulkan/present/ssaa.h
renderer_vulkan/present/layer.cpp
renderer_vulkan/present/layer.h
renderer_vulkan/present/present_push_constants.h
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt
index 9e8d76b104..c7a5882545 100644
--- a/src/video_core/host_shaders/CMakeLists.txt
+++ b/src/video_core/host_shaders/CMakeLists.txt
@@ -60,6 +60,7 @@ set(SHADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/smaa_blending_weight_calculation.frag
${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.vert
${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.frag
+ ${CMAKE_CURRENT_SOURCE_DIR}/ssaa.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_blit_depth_stencil.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.vert
diff --git a/src/video_core/host_shaders/ssaa.frag b/src/video_core/host_shaders/ssaa.frag
new file mode 100644
index 0000000000..cbc5d2f9d7
--- /dev/null
+++ b/src/video_core/host_shaders/ssaa.frag
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#version 460
+#extension GL_ARB_sample_shading : enable
+
+#ifdef VULKAN
+#define BINDING_COLOR_TEXTURE 1
+#else // ^^^ Vulkan ^^^ // vvv OpenGL vvv
+#define BINDING_COLOR_TEXTURE 0
+#endif
+
+layout (location = 0) in vec4 posPos;
+layout (location = 0) out vec4 frag_color;
+layout (binding = BINDING_COLOR_TEXTURE) uniform sampler2D input_texture;
+
+void main() {
+ frag_color = texelFetch(input_texture, ivec2(posPos.xy * textureSize(input_texture)), gl_SampleID);
+}
diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp
index e20473d2af..6238f56629 100644
--- a/src/video_core/renderer_vulkan/present/layer.cpp
+++ b/src/video_core/renderer_vulkan/present/layer.cpp
@@ -111,6 +111,8 @@ void Layer::ConfigureDraw(PresentPushConstants* out_push_constants,
fxaa->Draw(scheduler, image_index, &source_image, &source_image_view);
} else if (auto* smaa = std::get_if(&anti_alias)) {
smaa->Draw(scheduler, image_index, &source_image, &source_image_view);
+ } else if (auto* ssaa = std::get_if(&anti_alias)) {
+ ssaa->Draw(scheduler, image_index, &source_image, &source_image_view);
}
auto crop_rect = Tegra::NormalizeCrop(framebuffer, texture_width, texture_height);
diff --git a/src/video_core/renderer_vulkan/present/layer.h b/src/video_core/renderer_vulkan/present/layer.h
index 47a6a69218..ac8ed69b53 100644
--- a/src/video_core/renderer_vulkan/present/layer.h
+++ b/src/video_core/renderer_vulkan/present/layer.h
@@ -16,6 +16,7 @@
#include "video_core/renderer_vulkan/present/sgsr.h"
#include "video_core/renderer_vulkan/present/fxaa.h"
#include "video_core/renderer_vulkan/present/smaa.h"
+#include "video_core/renderer_vulkan/present/ssaa.h"
namespace Layout {
struct FramebufferLayout;
@@ -95,7 +96,7 @@ private:
Service::android::PixelFormat pixel_format{};
Settings::AntiAliasing anti_alias_setting{};
- std::variant anti_alias{};
+ std::variant anti_alias{};
std::variant sr_filter{};
std::vector resource_ticks{};
};
diff --git a/src/video_core/renderer_vulkan/present/ssaa.cpp b/src/video_core/renderer_vulkan/present/ssaa.cpp
new file mode 100644
index 0000000000..14d3b23a1d
--- /dev/null
+++ b/src/video_core/renderer_vulkan/present/ssaa.cpp
@@ -0,0 +1,131 @@
+// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/common_types.h"
+
+#include "video_core/host_shaders/ssaa_frag_spv.h"
+#include "video_core/host_shaders/full_screen_triangle_vert_spv.h"
+#include "video_core/renderer_vulkan/present/ssaa.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 {
+
+SSAA::SSAA(const Device& device, MemoryAllocator& allocator, size_t image_count, VkExtent2D extent)
+ : m_device(device), m_allocator(allocator), m_extent(extent)
+ , m_image_count(u32(image_count))
+{
+ CreateImages();
+ CreateRenderPasses();
+ CreateSampler();
+ CreateShaders();
+ CreateDescriptorPool();
+ CreateDescriptorSetLayouts();
+ CreateDescriptorSets();
+ CreatePipelineLayouts();
+ CreatePipelines();
+}
+
+SSAA::~SSAA() = default;
+
+void SSAA::CreateImages() {
+ for (u32 i = 0; i < m_image_count; i++) {
+ Image& image = m_dynamic_images.emplace_back();
+ image.image = CreateWrappedImage(m_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT);
+ image.image_view = CreateWrappedImageView(m_device, image.image, VK_FORMAT_R16G16B16A16_SFLOAT);
+ }
+}
+
+void SSAA::CreateRenderPasses() {
+ m_renderpass = CreateWrappedRenderPass(m_device, VK_FORMAT_R16G16B16A16_SFLOAT);
+ for (auto& image : m_dynamic_images) {
+ image.framebuffer = CreateWrappedFramebuffer(m_device, m_renderpass, image.image_view, m_extent);
+ }
+}
+
+void SSAA::CreateSampler() {
+ m_sampler = CreateWrappedSampler(m_device);
+}
+
+void SSAA::CreateShaders() {
+ m_vertex_shader = CreateWrappedShaderModule(m_device, FULL_SCREEN_TRIANGLE_VERT_SPV);
+ m_fragment_shader = CreateWrappedShaderModule(m_device, SSAA_FRAG_SPV);
+}
+
+void SSAA::CreateDescriptorPool() {
+ // 2 descriptors, 1 descriptor set per image
+ m_descriptor_pool = CreateWrappedDescriptorPool(m_device, 2 * m_image_count, m_image_count);
+}
+
+void SSAA::CreateDescriptorSetLayouts() {
+ m_descriptor_set_layout = CreateWrappedDescriptorSetLayout(m_device, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER});
+}
+
+void SSAA::CreateDescriptorSets() {
+ VkDescriptorSetLayout layout = *m_descriptor_set_layout;
+ for (auto& images : m_dynamic_images)
+ images.descriptor_sets = CreateWrappedDescriptorSets(m_descriptor_pool, {layout});
+}
+
+void SSAA::CreatePipelineLayouts() {
+ m_pipeline_layout = CreateWrappedPipelineLayout(m_device, m_descriptor_set_layout);
+}
+
+void SSAA::CreatePipelines() {
+ m_pipeline = CreateWrappedPipeline(m_device, m_renderpass, m_pipeline_layout, std::tie(m_vertex_shader, m_fragment_shader));
+}
+
+void SSAA::UpdateDescriptorSets(VkImageView image_view, size_t image_index) {
+ Image& image = 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, image.descriptor_sets[0], 0));
+ updates.push_back(CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, image.descriptor_sets[0], 1));
+ m_device.GetLogical().UpdateDescriptorSets(updates, {});
+}
+
+void SSAA::UploadImages(Scheduler& scheduler) {
+ if (!m_images_ready) {
+ m_images_ready = true;
+ scheduler.Record([&](vk::CommandBuffer cmdbuf) {
+ for (auto& image : m_dynamic_images)
+ ClearColorImage(cmdbuf, *image.image);
+ });
+ scheduler.Finish();
+ }
+}
+
+void SSAA::Draw(Scheduler& scheduler, size_t image_index, VkImage* inout_image, VkImageView* inout_image_view) {
+ const Image& image{m_dynamic_images[image_index]};
+ const VkImage input_image{*inout_image};
+ const VkImage output_image{*image.image};
+ const VkDescriptorSet descriptor_set{image.descriptor_sets[0]};
+ const VkFramebuffer framebuffer{*image.framebuffer};
+ const VkRenderPass renderpass{*m_renderpass};
+ const VkPipeline pipeline{*m_pipeline};
+ const VkPipelineLayout layout{*m_pipeline_layout};
+ const VkExtent2D extent{m_extent};
+
+ UploadImages(scheduler);
+ UpdateDescriptorSets(*inout_image_view, image_index);
+
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([=](vk::CommandBuffer cmdbuf) {
+ TransitionImageLayout(cmdbuf, input_image, VK_IMAGE_LAYOUT_GENERAL);
+ TransitionImageLayout(cmdbuf, output_image, VK_IMAGE_LAYOUT_GENERAL);
+ BeginRenderPass(cmdbuf, renderpass, framebuffer, extent);
+ cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
+ cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, descriptor_set, {});
+ cmdbuf.Draw(3, 1, 0, 0);
+ cmdbuf.EndRenderPass();
+ TransitionImageLayout(cmdbuf, output_image, VK_IMAGE_LAYOUT_GENERAL);
+ });
+
+ *inout_image = *image.image;
+ *inout_image_view = *image.image_view;
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/present/ssaa.h b/src/video_core/renderer_vulkan/present/ssaa.h
new file mode 100644
index 0000000000..d067c85556
--- /dev/null
+++ b/src/video_core/renderer_vulkan/present/ssaa.h
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "video_core/renderer_vulkan/present/anti_alias_pass.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 StagingBufferPool;
+
+class SSAA final : public AntiAliasPass {
+public:
+ explicit SSAA(const Device& device, MemoryAllocator& allocator, size_t image_count,
+ VkExtent2D extent);
+ ~SSAA() override;
+
+ void Draw(Scheduler& scheduler, size_t image_index, VkImage* inout_image,
+ VkImageView* inout_image_view) override;
+
+private:
+ void CreateImages();
+ void CreateRenderPasses();
+ void CreateSampler();
+ void CreateShaders();
+ void CreateDescriptorPool();
+ void CreateDescriptorSetLayouts();
+ void CreateDescriptorSets();
+ void CreatePipelineLayouts();
+ void CreatePipelines();
+ void UpdateDescriptorSets(VkImageView image_view, size_t image_index);
+ void UploadImages(Scheduler& scheduler);
+
+ const Device& m_device;
+ MemoryAllocator& m_allocator;
+ const VkExtent2D m_extent;
+ const u32 m_image_count;
+
+ vk::ShaderModule m_vertex_shader{};
+ vk::ShaderModule m_fragment_shader{};
+ vk::DescriptorPool m_descriptor_pool{};
+ vk::DescriptorSetLayout m_descriptor_set_layout{};
+ vk::PipelineLayout m_pipeline_layout{};
+ vk::Pipeline m_pipeline{};
+ vk::RenderPass m_renderpass{};
+
+ struct Image {
+ vk::DescriptorSets descriptor_sets{};
+ vk::Framebuffer framebuffer{};
+ vk::Image image{};
+ vk::ImageView image_view{};
+ };
+ std::vector m_dynamic_images{};
+ bool m_images_ready{};
+
+ vk::Sampler m_sampler{};
+};
+
+} // namespace Vulkan