From 736189ea772cb5fa5b5020a796bc4e750d226c1e Mon Sep 17 00:00:00 2001 From: Wunk Date: Thu, 5 Nov 2026 04:35:12 -0800 Subject: [PATCH] video_core: Implement MSAA (#2053) * config: Add `sample_count` renderer option Option is only enabled when the renderer is set to Vulkan, for now. * vk_blit_helper: Add `d24s8_to_rgba8_ms_comp` Helper host-shader for blitting multi-sampled DS24S8 textures to multi-sampled RGBA8 * vk_instance: Add detection of MSAA features Full multi-sample support is when renderpass-2 and depth-stencil-resolve extensions are available and when sample-rate-shading and msaa-storage-images are supported. * rasterizer_cache: Initial support for multi-sample surfaces * renderer_vulkan: Implement multisample pipeline/renderpass support Allows multi-sample render passes and graphics pipelines to be created, using sample-rate shading rather than coverage-based MSAA. * renderer_vulkan: Implement multisample texture runtime This seems to be enough for simple programs to render with MSAA enabled! * renderer_vulkan: Fix multisample texture init barrier Should address the MultiSampled image directly since the multisampled image is just a transient image and not the leading state of the image. * renderer_vulkan: Fix `Framebuffer::sample_count` move-operator `sample_count` needs to be move/copied over. Also reorder the accessor order to match the declaration of variables. * renderer_vulkan: Fix dangling surface reference during msaa resolve These individual parameters need to be copied as the reference to the surface-object only lasts within the scope of this function. * renderer_vulkan: Derive framebuffer sample-count from attachments Derive the framebuffer sample-count from the input color and depth operands. Similar to how `res_scale` is determined. * renderer_vulkan: Fix initialization of image handles Try to optimally create the new image handles when a change in res scale or sample-count has actually occured. MSAA images need to be updated too in the case that the resolution scale has changed * renderer_vulkan: Fix cleanup and debug-naming for `d24s8_to_rgba8_ms_comp` * renderer_vulkan: Add `TextureRuntime::ResolveTexture` Rather than use a big lambda, just rip this out into a proper function for other blit functions to utilize. * renderer_vulkan: Fix narrowing byte conversion Fixes a compilation error on Unix platforms. * renderer_vulkan: Fix MSAA framebuffer target resolve surface Use the specified type rather than defaulting to the surface's current one(implicit `ImageView` argument). * renderer_vulkan: Fix `Surface::ScaleUp` scale/sample increase This should be checking the _new_ value to possibly cull upscaled texture creation rather than the current value of the surface. Fixes broken up render passes when drawing UI in some games. * renderer_vulkan: Fix `ConvertDS24S8ToRGBA8` image targets Ensure that the Multi-Sample texture is used for the destination color image as well. Should the dest image be MSAA too? Or should all the values be resolved into a minimum depth and some combination of stencil-values here? * renderer_vulkan: Fix multisample `ClearTextureWithRenderpass` Ensure that the multisample framebuffer is used rather than the usual one when using a render pass to clear the textures. That way, a MSAA-render AND resolve is used to clear the textures at the same time. * renderer_vulkan: Fix multisample framebuffer creation MSAA renderpasses require the "current" image as the main resolve target, and the MSAA image as the second attachment. * renderer_vulkan: Fix MSAA image debug name * renderer_vulkan: Fix Multisample `ConvertDS24S8ToRGBA8` Migrate `ResolveTexture` to `BlitHelper`. If the conversion was multi-sample, do a final resolve at the end of the conversion to ensure the non-multisample textures are updated as well. Fixes Pokemon! * config: Refactor `sample_count` to `antialiasing` setting Intended to allow for other anti-aliasing methods to be introduced * renderer_gl: Initial MSAA implementation Basically copied over some of the paradigms over from Vulkan. Covers most rendering uses-cases except for conversions such as `ConvertDS24S8ToRGBA8` and `ConvertRGBA4ToRGB5A1` * renderer_gl: Fix surface `ScaleUp` implementation Should assign the new `res_scale` and `sample_count` * renderer_gl: Implement multisample `Convert{DS24S8ToRGBA8,RGBA4ToRGB5A1}` * renderer_gl: Resolve framebuffer after each draw Brute force approach while trying to determine a better heuristic. Framebuffer changes are not enough to determine the end of a "render pass". An 'msaa dirty flag' is likely the better way here. * renderer_gl: Implement `TextureRuntime::ClearTexture` MSAA clears When ArbClearTexture is not available, ensure the fallback implementation also clears the MSAA texture as well. * renderer_gl: Fix MakeHandle string-view hazard `glObjectLabel` expects a null-terminated string, but an `std::string_view` is not necessarily null-terminated * renderer_gl: Add layer/level specifier to MSAA resolves * renderer_vulkan: Conditionally compile `DS24S8->RGBA8` multisample pipeline This will crash on apple due to the lack of support for MSAA texture storage operations. This gets MacOS running generally now, but will likely require a refactor of some kind into a fragment shader and graphics pipeline rather than a compute shader for MacOS in particular. * config: Stub support for up to `MSAAx16` This is primarily to claim the enum value before other anti-aliasing methods get added. Eventually, support for the RHI device's sample-count will have to be detected and used opportunistically. Maybe "MSAA" is the anti-aliasing method and the actual sample-count comes from someplace else? * config: Disable `MSAAx{8,16}` options by default These are not guaranteed to be supported and should be disabled by default for now. We still want the config options to be allocated. They should just not user-facing until these values can be conditionally enabled with fallbacks. * renderer_vulkan: Remove unneeded sample-count info from `RasterizerVulkan::AccelerateDisplay` We're always resolving into the best non-msaa image. This isnt needed here. OpenGL doesn't need it either. * core: Fix license headers Also fix some clang-format formatting * renderer_vulkan: Disable MSAA functionality from MacOS The features required to support this at the moment are out of the scope of this PR, thus MSAA is disabled for now and will be re-enabled in a later PR. All MSAA anti-aliasing settings are removed when running on MacOS. The main features holding this back are: * Dynamically detecting the number of samples that the host GPU supports(up to x4 is required, but x8 and x16 need to be detected when initializing the backend) * Blit-helper needs to have non-compute-shader variants of its MSAA reinterpretation functions, but Metal(MoltenVK) does not support writing to individual MSAA samples from a compute shader, and instead must be done from a regular fragment-shader from within a render-pass. * renderer_vulkan: Fix recycling of MSAA textures Fixes an issue where reducing the sample count at run-time was causing crashes due to previous MSAA textures, with a higher sample count, being re-used. This also re-orders the criteria for recycling sentenced textures to allow for better short-circuiting. The criteria for a texture to be recycled must strictly be the "ScaleUp" case, and not the case where the sample-count is being _reduced_. This recycling could be a bit smarter to consider cases where a texture can have its sample-count reduced but not its resolution scale. But that's an optimization that is out of the scope of this PR, I feel. * renderer_vulkan: Fix enabled extension array size There are 16 total calls to `add_extension` that indicates that the `enabled_extensions` array should be 16 rather than 13. * renderer_vulkan: Disable MSAA functionality from UI This reverts commit 26939156c0902c77121edcbb9cb798accc776103. MSAA requires some advanced configuration features that are not quite ready yet. Currently, `shaderStorageImageMultisample` support needs to be detected at config-time to allow MSAA to be used. Otherwise, the reinterpret-passes need to have a non-compute alternative implemented. Both are pretty larege features that should not increase the scale of this PR for now. To mitigate people from interfacing with this feature for now, anti-aliasing options that involve MSAA will be disabled from the UI and only operable from editing the ini configurations. This is to allow for other anti-aliasing methods to be allowed in the team time. --- CMakeModules/GenerateSettingKeys.cmake | 1 + src/android/app/src/main/jni/config.cpp | 1 + src/citra_qt/configuration/config.cpp | 2 + .../configuration/configure_enhancements.cpp | 33 ++- .../configuration/configure_enhancements.ui | 68 ++++- src/common/settings.cpp | 2 + src/common/settings.h | 24 ++ src/video_core/host_shaders/CMakeLists.txt | 5 +- .../d24s8_to_rgba8_ms.frag | 25 ++ .../rgba4_to_rgb5a1_ms.frag | 22 ++ .../vulkan_d24s8_to_rgba8_ms.comp | 31 +++ .../rasterizer_cache/rasterizer_cache.h | 58 ++++- .../rasterizer_cache/rasterizer_cache_base.h | 5 +- .../rasterizer_cache/surface_params.cpp | 15 +- .../rasterizer_cache/surface_params.h | 13 +- src/video_core/renderer_base.cpp | 4 + src/video_core/renderer_base.h | 3 + .../renderer_opengl/gl_blit_helper.cpp | 111 ++++++-- .../renderer_opengl/gl_blit_helper.h | 13 +- .../renderer_opengl/gl_rasterizer.cpp | 23 +- .../renderer_opengl/gl_texture_runtime.cpp | 203 ++++++++++----- .../renderer_opengl/gl_texture_runtime.h | 20 +- .../renderer_vulkan/vk_blit_helper.cpp | 148 ++++++++++- .../renderer_vulkan/vk_blit_helper.h | 8 +- .../renderer_vulkan/vk_graphics_pipeline.cpp | 8 +- .../renderer_vulkan/vk_graphics_pipeline.h | 3 +- .../renderer_vulkan/vk_instance.cpp | 10 +- src/video_core/renderer_vulkan/vk_instance.h | 11 +- .../renderer_vulkan/vk_rasterizer.cpp | 1 + .../renderer_vulkan/vk_render_manager.cpp | 179 +++++++++++-- .../renderer_vulkan/vk_render_manager.h | 25 +- .../renderer_vulkan/vk_shader_disk_cache.h | 4 +- .../renderer_vulkan/vk_texture_runtime.cpp | 246 ++++++++++++------ .../renderer_vulkan/vk_texture_runtime.h | 35 ++- 34 files changed, 1086 insertions(+), 274 deletions(-) create mode 100644 src/video_core/host_shaders/format_reinterpreter/d24s8_to_rgba8_ms.frag create mode 100644 src/video_core/host_shaders/format_reinterpreter/rgba4_to_rgb5a1_ms.frag create mode 100644 src/video_core/host_shaders/format_reinterpreter/vulkan_d24s8_to_rgba8_ms.comp diff --git a/CMakeModules/GenerateSettingKeys.cmake b/CMakeModules/GenerateSettingKeys.cmake index 75e872f55..fc3ded319 100644 --- a/CMakeModules/GenerateSettingKeys.cmake +++ b/CMakeModules/GenerateSettingKeys.cmake @@ -50,6 +50,7 @@ foreach(KEY IN ITEMS "use_display_refresh_rate_detection" "use_shader_jit" "resolution_factor" + "antialiasing" "frame_limit" "turbo_limit" "texture_filter" diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 481327ce1..ccde567a0 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -210,6 +210,7 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.use_hw_shader); ReadSetting("Renderer", Settings::values.use_shader_jit); ReadSetting("Renderer", Settings::values.resolution_factor); + ReadSetting("Renderer", Settings::values.antialiasing); ReadSetting("Renderer", Settings::values.use_disk_shader_cache); ReadSetting("Renderer", Settings::values.use_vsync); ReadSetting("Renderer", Settings::values.use_skip_duplicate_frames); diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 513316a53..87c4423a7 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -730,6 +730,7 @@ void QtConfig::ReadRendererValues() { ReadGlobalSetting(Settings::values.use_skip_duplicate_frames); ReadGlobalSetting(Settings::values.use_display_refresh_rate_detection); ReadGlobalSetting(Settings::values.resolution_factor); + ReadGlobalSetting(Settings::values.antialiasing); ReadGlobalSetting(Settings::values.use_integer_scaling); ReadGlobalSetting(Settings::values.frame_limit); ReadGlobalSetting(Settings::values.turbo_limit); @@ -1288,6 +1289,7 @@ void QtConfig::SaveRendererValues() { WriteGlobalSetting(Settings::values.use_skip_duplicate_frames); WriteGlobalSetting(Settings::values.use_display_refresh_rate_detection); WriteGlobalSetting(Settings::values.resolution_factor); + WriteGlobalSetting(Settings::values.antialiasing); WriteGlobalSetting(Settings::values.use_integer_scaling); WriteGlobalSetting(Settings::values.frame_limit); WriteGlobalSetting(Settings::values.turbo_limit); diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp index a63fe77fa..b8720c364 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -1,8 +1,9 @@ // Copyright 2019-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #include +#include #include "citra_qt/configuration/configuration_shared.h" #include "citra_qt/configuration/configure_enhancements.h" #include "common/settings.h" @@ -19,8 +20,21 @@ ConfigureEnhancements::ConfigureEnhancements(QWidget* parent) SetConfiguration(); const auto graphics_api = Settings::GetWorkingGraphicsAPI(); - const bool res_scale_enabled = graphics_api != Settings::GraphicsAPI::Software; - ui->resolution_factor_combobox->setEnabled(res_scale_enabled); + const bool hardware_graphics = graphics_api != Settings::GraphicsAPI::Software; + ui->resolution_factor_combobox->setEnabled(hardware_graphics); + ui->antialiasing_combobox->setEnabled(hardware_graphics); + + // TODO(wunk): MSAA requires GPU capabilities to be understood at config-time before the backend + // has actually been created. Disable all UI MSAA options until this feature is available. + // Hide all AntiAliasing options that involve MSAA. + QListView* combobox_view = qobject_cast(ui->antialiasing_combobox->view()); + for (int item_index = 0; item_index < ui->antialiasing_combobox->count(); ++item_index) { + if (Settings::GetAntiAliasingSampleCount( + static_cast(item_index)) > 1) { + combobox_view->setRowHidden(item_index, true); + ui->antialiasing_combobox->setItemData(item_index, 0, Qt::UserRole - 1); + } + } connect(ui->render_3d_combobox, qOverload(&QComboBox::currentIndexChanged), this, [this](int currentIndex) { @@ -44,6 +58,8 @@ void ConfigureEnhancements::SetConfiguration() { if (!Settings::IsConfiguringGlobal()) { ConfigurationShared::SetPerGameSetting(ui->resolution_factor_combobox, &Settings::values.resolution_factor); + ConfigurationShared::SetPerGameSetting(ui->antialiasing_combobox, + &Settings::values.antialiasing); ConfigurationShared::SetPerGameSetting(ui->texture_filter_combobox, &Settings::values.texture_filter); ConfigurationShared::SetHighlight(ui->widget_texture_filter, @@ -51,6 +67,8 @@ void ConfigureEnhancements::SetConfiguration() { } else { ui->resolution_factor_combobox->setCurrentIndex( Settings::values.resolution_factor.GetValue()); + ui->antialiasing_combobox->setCurrentIndex( + static_cast(Settings::values.antialiasing.GetValue())); ui->texture_filter_combobox->setCurrentIndex( static_cast(Settings::values.texture_filter.GetValue())); } @@ -111,6 +129,8 @@ void ConfigureEnhancements::RetranslateUI() { void ConfigureEnhancements::ApplyConfiguration() { ConfigurationShared::ApplyPerGameSetting(&Settings::values.resolution_factor, ui->resolution_factor_combobox); + ConfigurationShared::ApplyPerGameSetting(&Settings::values.antialiasing, + ui->antialiasing_combobox); Settings::values.render_3d = static_cast(ui->render_3d_combobox->currentIndex()); Settings::values.swap_eyes_3d = ui->swap_eyes_3d->isChecked(); @@ -149,6 +169,7 @@ void ConfigureEnhancements::SetupPerGameUI() { // Block the global settings if a game is currently running that overrides them if (Settings::IsConfiguringGlobal()) { ui->widget_resolution->setEnabled(Settings::values.resolution_factor.UsingGlobal()); + ui->widget_antialiasing->setEnabled(Settings::values.antialiasing.UsingGlobal()); ui->widget_texture_filter->setEnabled(Settings::values.texture_filter.UsingGlobal()); ui->toggle_linear_filter->setEnabled(Settings::values.filter_mode.UsingGlobal()); ui->use_integer_scaling->setEnabled(Settings::values.use_integer_scaling.UsingGlobal()); @@ -189,6 +210,10 @@ void ConfigureEnhancements::SetupPerGameUI() { ui->resolution_factor_combobox, ui->widget_resolution, static_cast(Settings::values.resolution_factor.GetValue(true))); + ConfigurationShared::SetColoredComboBox( + ui->antialiasing_combobox, ui->widget_antialiasing, + static_cast(Settings::values.antialiasing.GetValue(true))); + ConfigurationShared::SetColoredComboBox( ui->texture_filter_combobox, ui->widget_texture_filter, static_cast(Settings::values.texture_filter.GetValue(true))); diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui index b6ef43e02..0f288ca23 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -148,15 +148,69 @@ - - - Use Integer Scaling - - - <html><head/><body><p>Use Integer Scaling</p><p>Enforces that the larger screen in all layouts is an integer scale of the 240px height of the original 3DS screen.</p></body></html> - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Anti-Aliasing + + + + + + + + None + + + + + MSAAx2 + + + + + MSAAx4 + + + + + MSAAx8 + + + + + MSAAx16 + + + + + + + + + Use Integer Scaling + + + <html><head/><body><p>Use Integer Scaling</p><p>Enforces that the larger screen in all layouts is an integer scale of the 240px height of the original 3DS screen.</p></body></html> + + + diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 0b38be150..175845ec8 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -102,6 +102,7 @@ void LogSettings() { log_setting("Renderer_ShadersAccurateMul", values.shaders_accurate_mul.GetValue()); log_setting("Renderer_UseShaderJit", values.use_shader_jit.GetValue()); log_setting("Renderer_UseResolutionFactor", values.resolution_factor.GetValue()); + log_setting("Renderer_SampleCount", values.antialiasing.GetValue()); log_setting("Renderer_UseIntegerScaling", values.use_integer_scaling.GetValue()); log_setting("Renderer_FrameLimit", values.frame_limit.GetValue()); log_setting("Renderer_VSyncNew", values.use_vsync.GetValue()); @@ -222,6 +223,7 @@ void RestoreGlobalState(bool is_powered_on) { values.use_vsync.SetGlobal(true); values.use_skip_duplicate_frames.SetGlobal(true); values.resolution_factor.SetGlobal(true); + values.antialiasing.SetGlobal(true); values.use_integer_scaling.SetGlobal(true); values.frame_limit.SetGlobal(true); values.texture_filter.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index c359136db..cddbc18f2 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -119,6 +119,29 @@ enum class AudioEmulation : u32 { LLEMultithreaded = 2, }; +enum class AntiAliasingMethod : u32 { + None = 0, + MSAAx2 = 1, + MSAAx4 = 2, + MSAAx8 = 3, + MSAAx16 = 4, +}; + +static inline u8 GetAntiAliasingSampleCount(AntiAliasingMethod antialiasing_method) { + switch (antialiasing_method) { + case AntiAliasingMethod::MSAAx2: + return 2; + case AntiAliasingMethod::MSAAx4: + return 4; + case AntiAliasingMethod::MSAAx8: + return 8; + case AntiAliasingMethod::MSAAx16: + return 16; + default: + return 1; + } +} + enum class TextureFilter : u32 { NoFilter = 0, Anime4K = 1, @@ -559,6 +582,7 @@ struct Values { true, Keys::use_display_refresh_rate_detection}; Setting use_shader_jit{true, Keys::use_shader_jit}; SwitchableSetting resolution_factor{1, 0, 18, Keys::resolution_factor}; + SwitchableSetting antialiasing{AntiAliasingMethod::None, "antialiasing"}; SwitchableSetting use_integer_scaling{false, Keys::use_integer_scaling}; SwitchableSetting frame_limit{100, 0, 1000, Keys::frame_limit}; SwitchableSetting turbo_limit{200, 0, 1000, Keys::turbo_limit}; diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 964000d92..939c06c9f 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -1,11 +1,14 @@ -# Copyright 2023 Citra Emulator Project +# Copyright Citra Emulator Project / Azahar Emulator Project # Licensed under GPLv2 or any later version # Refer to the license.txt file included. set(SHADER_FILES format_reinterpreter/d24s8_to_rgba8.frag + format_reinterpreter/d24s8_to_rgba8_ms.frag format_reinterpreter/rgba4_to_rgb5a1.frag + format_reinterpreter/rgba4_to_rgb5a1_ms.frag format_reinterpreter/vulkan_d24s8_to_rgba8.comp + format_reinterpreter/vulkan_d24s8_to_rgba8_ms.comp texture_filtering/bicubic.frag texture_filtering/refine.frag texture_filtering/scale_force.frag diff --git a/src/video_core/host_shaders/format_reinterpreter/d24s8_to_rgba8_ms.frag b/src/video_core/host_shaders/format_reinterpreter/d24s8_to_rgba8_ms.frag new file mode 100644 index 000000000..54b6e831e --- /dev/null +++ b/src/video_core/host_shaders/format_reinterpreter/d24s8_to_rgba8_ms.frag @@ -0,0 +1,25 @@ +// Copyright 2026 Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. + +//? #version 430 core + +precision highp int; +precision highp float; + +layout(location = 0) in mediump vec2 tex_coord; +layout(location = 0) out lowp vec4 frag_color; + +layout(binding = 0) uniform highp sampler2DMS depth; +layout(binding = 1) uniform lowp usampler2DMS stencil; + +void main() { + mediump vec2 coord = tex_coord * vec2(textureSize(depth)); + mediump ivec2 tex_icoord = ivec2(coord); + highp uint depth_val = + uint(texelFetch(depth, tex_icoord, gl_SampleID).x * (exp2(32.0) - 1.0)); + lowp uint stencil_val = texelFetch(stencil, tex_icoord, gl_SampleID).x; + highp uvec4 components = + uvec4(stencil_val, (uvec3(depth_val) >> uvec3(24u, 16u, 8u)) & 0x000000FFu); + frag_color = vec4(components) / (exp2(8.0) - 1.0); +} diff --git a/src/video_core/host_shaders/format_reinterpreter/rgba4_to_rgb5a1_ms.frag b/src/video_core/host_shaders/format_reinterpreter/rgba4_to_rgb5a1_ms.frag new file mode 100644 index 000000000..06b6bf7a9 --- /dev/null +++ b/src/video_core/host_shaders/format_reinterpreter/rgba4_to_rgb5a1_ms.frag @@ -0,0 +1,22 @@ +// Copyright 2023-2026 Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. + +//? #version 430 core + +precision highp int; +precision highp float; + +layout(location = 0) in mediump vec2 tex_coord; +layout(location = 0) out lowp vec4 frag_color; + +layout(binding = 0) uniform lowp sampler2D source; + +void main() { + mediump vec2 coord = tex_coord * vec2(textureSize(source, 0)); + mediump ivec2 tex_icoord = ivec2(coord); + lowp ivec4 rgba4 = ivec4(texelFetch(source, tex_icoord, 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); +} diff --git a/src/video_core/host_shaders/format_reinterpreter/vulkan_d24s8_to_rgba8_ms.comp b/src/video_core/host_shaders/format_reinterpreter/vulkan_d24s8_to_rgba8_ms.comp new file mode 100644 index 000000000..62297a202 --- /dev/null +++ b/src/video_core/host_shaders/format_reinterpreter/vulkan_d24s8_to_rgba8_ms.comp @@ -0,0 +1,31 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#version 450 core +#extension GL_EXT_samplerless_texture_functions : require + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; +layout(set = 0, binding = 0) uniform highp texture2DMS depth; +layout(set = 0, binding = 1) uniform lowp utexture2DMS stencil; +layout(set = 0, binding = 2, rgba8) uniform highp writeonly image2DMS color; + +layout(push_constant, std140) uniform ComputeInfo { + mediump ivec2 src_offset; + mediump ivec2 dst_offset; + mediump ivec2 extent; +}; + +void main() { + ivec2 src_coord = src_offset + ivec2(gl_GlobalInvocationID.xy); + ivec2 dst_coord = dst_offset + ivec2(gl_GlobalInvocationID.xy); + + int sample_count = textureSamples(depth); + for(int sample_index = 0; sample_index < sample_count; ++sample_index) + { + highp uint depth_val = uint(texelFetch(depth, src_coord, sample_index).x * (exp2(32.0) - 1.0)); + lowp uint stencil_val = texelFetch(stencil, src_coord, sample_index).x; + highp uvec4 components = uvec4(stencil_val, (uvec3(depth_val) >> uvec3(24u, 16u, 8u)) & 0x000000FFu); + imageStore(color, dst_coord, sample_index, vec4(components) / (exp2(8.0) - 1.0)); + } +} diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index 0aab5596d..7313f0d48 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -38,7 +38,7 @@ RasterizerCache::RasterizerCache(Memory::MemorySystem& memory_, Pica::RegsInternal& regs_, RendererBase& renderer_) : memory{memory_}, custom_tex_manager{custom_tex_manager_}, runtime{runtime_}, regs{regs_}, renderer{renderer_}, resolution_scale_factor{renderer.GetResolutionScaleFactor()}, - filter{Settings::values.texture_filter.GetValue()}, + sample_count{renderer.GetSampleCount()}, filter{Settings::values.texture_filter.GetValue()}, dump_textures{Settings::values.dump_textures.GetValue()}, use_custom_textures{Settings::values.custom_textures.GetValue()} { using TextureConfig = Pica::TexturingRegs::TextureConfig; @@ -112,12 +112,15 @@ void RasterizerCache::TickFrame() { } const u32 scale_factor = renderer.GetResolutionScaleFactor(); + const u32 samples = renderer.GetSampleCount(); const bool resolution_scale_changed = resolution_scale_factor != scale_factor; + const bool sample_count_changed = sample_count != samples; const bool use_custom_texture_changed = Settings::values.custom_textures.GetValue() != use_custom_textures; - if (resolution_scale_changed || use_custom_texture_changed) { + if (resolution_scale_changed || use_custom_texture_changed || sample_count_changed) { resolution_scale_factor = scale_factor; + sample_count = renderer.GetSampleCount(); use_custom_textures = Settings::values.custom_textures.GetValue(); if (use_custom_textures) { custom_tex_manager.FindCustomTextures(); @@ -307,6 +310,7 @@ bool RasterizerCache::AccelerateDisplayTransfer(const Pica::DisplayTransferCo : config.output_height.Value(); dst_params.is_tiled = config.input_linear != config.dont_swizzle; dst_params.pixel_format = PixelFormatFromGPUPixelFormat(config.output_format); + dst_params.sample_count = sample_count; dst_params.UpdateParams(); // Using flip_vertically alongside crop_input_lines produces skewed output on hardware. @@ -322,6 +326,7 @@ bool RasterizerCache::AccelerateDisplayTransfer(const Pica::DisplayTransferCo } dst_params.res_scale = slot_surfaces[src_surface_id].res_scale; + dst_params.sample_count = slot_surfaces[src_surface_id].sample_count; const auto [dst_surface_id, dst_rect] = GetSurfaceSubRect(dst_params, ScaleMatch::Upscale, false); @@ -452,8 +457,10 @@ void RasterizerCache::CopySurface(Surface& src_surface, Surface& dst_surface, const u32 src_scale = src_surface.res_scale; const u32 dst_scale = dst_surface.res_scale; - if (src_scale > dst_scale) { - dst_surface.ScaleUp(src_scale); + const u32 src_sample_count = src_surface.sample_count; + const u32 dst_sample_count = dst_surface.sample_count; + if ((src_scale > dst_scale) || (src_sample_count > dst_sample_count)) { + dst_surface.ScaleUp(src_scale, src_sample_count); } const auto src_rect = src_surface.GetScaledSubRect(subrect_params); @@ -522,6 +529,7 @@ typename RasterizerCache::SurfaceRect_Tuple RasterizerCache::GetSurfaceSub if (surface_id) { SurfaceParams new_params = slot_surfaces[surface_id]; new_params.res_scale = params.res_scale; + new_params.sample_count = params.sample_count; surface_id = CreateSurface(new_params, create_initial_flags); RegisterSurface(surface_id); @@ -727,6 +735,7 @@ FramebufferHelper RasterizerCache::GetFramebufferSurfaces(bool using_color SurfaceParams color_params; color_params.is_tiled = true; color_params.res_scale = resolution_scale_factor; + color_params.sample_count = sample_count; color_params.width = config.GetWidth(); color_params.height = config.GetHeight(); SurfaceParams depth_params = color_params; @@ -886,12 +895,16 @@ SurfaceId RasterizerCache::FindMatch(const SurfaceParams& params, ScaleMatch SurfaceId match_id{}; bool match_valid = false; u32 match_scale = 0; + u8 match_sample_count = 0; SurfaceInterval match_interval{}; ForEachSurfaceInRegion(params.addr, params.size, [&](SurfaceId surface_id, Surface& surface) { const bool res_scale_matched = match_scale_type == ScaleMatch::Exact ? (params.res_scale == surface.res_scale) : (params.res_scale <= surface.res_scale); + const bool sample_count_matched = match_scale_type == ScaleMatch::Exact + ? (params.sample_count == surface.sample_count) + : (params.sample_count <= surface.sample_count); const bool is_valid = True(find_flags & MatchFlags::Copy) ? true @@ -911,11 +924,16 @@ SurfaceId RasterizerCache::FindMatch(const SurfaceParams& params, ScaleMatch surface.type != SurfaceType::Fill) return; + if (!sample_count_matched && match_scale_type != ScaleMatch::Ignore && + surface.type != SurfaceType::Fill) + return; + // Found a match, update only if this is better than the previous one auto UpdateMatch = [&] { match_id = surface_id; match_valid = is_valid; match_scale = surface.res_scale; + match_sample_count = surface.sample_count; match_interval = surface_interval; }; @@ -926,6 +944,13 @@ SurfaceId RasterizerCache::FindMatch(const SurfaceParams& params, ScaleMatch return; } + if (surface.sample_count > match_sample_count) { + UpdateMatch(); + return; + } else if (surface.sample_count < match_sample_count) { + return; + } + if (is_valid && !match_valid) { UpdateMatch(); return; @@ -1288,8 +1313,9 @@ bool RasterizerCache::ValidateByReinterpretation(Surface& surface, SurfacePar return false; } const u32 res_scale = src_surface.res_scale; - if (res_scale > surface.res_scale) { - surface.ScaleUp(res_scale); + const u8 sample_count = src_surface.sample_count; + if ((res_scale > surface.res_scale) || (sample_count > surface.sample_count)) { + surface.ScaleUp(res_scale, sample_count); } const PAddr addr = boost::icl::lower(interval); const SurfaceParams copy_params = surface.FromInterval(copy_interval); @@ -1463,12 +1489,16 @@ SurfaceId RasterizerCache::CreateSurface(const SurfaceParams& params, const u64 resource_free_tick = runtime.GetResourceFreeTick(); // Try to find a matching texture in the deletion queue const auto it = std::find_if(sentenced.begin(), sentenced.end(), [&](const auto& pair) { - return (slot_surfaces[pair.first] == params) && - // Only recycle the texture if its resolution scale is equal or less than the - // incoming texture - (slot_surfaces[pair.first].res_scale <= params.res_scale) && - // Only recycle the texture if the texture runtime is completely done with it - (resource_free_tick > pair.second); + return + // Only recycle the texture if: + // The texture-runtime is completely done with it + (resource_free_tick > pair.second) + // The surface parameters match the incoming texture + && (slot_surfaces[pair.first] == params) + // Its resolution scale is equal or less than the incoming texture + && (slot_surfaces[pair.first].res_scale <= params.res_scale) + // Its sample count is equal or less than the incoming texture + && (slot_surfaces[pair.first].sample_count <= params.sample_count); }); // If a matching texture was found in the deletion queue, recycle it. if (it != sentenced.end()) { @@ -1479,10 +1509,10 @@ SurfaceId RasterizerCache::CreateSurface(const SurfaceParams& params, return slot_surfaces.insert(runtime, params, initial_flags); }(); Surface& surface = slot_surfaces[surface_id]; - if (params.res_scale > surface.res_scale) { + if ((params.res_scale > surface.res_scale) || (params.sample_count > surface.sample_count)) { // Texture is going to be upscaled, remove any previous framebuffer references RemoveFramebuffers(surface_id); - surface.ScaleUp(params.res_scale); + surface.ScaleUp(params.res_scale, params.sample_count); } surface.MarkInvalid(surface.GetInterval()); return surface_id; diff --git a/src/video_core/rasterizer_cache/rasterizer_cache_base.h b/src/video_core/rasterizer_cache/rasterizer_cache_base.h index 86c49c99e..0f5883fb1 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache_base.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache_base.h @@ -1,6 +1,6 @@ // Copyright 2023-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #pragma once @@ -227,6 +227,7 @@ private: SurfaceMap dirty_regions; PageMap cached_pages; u32 resolution_scale_factor; + u8 sample_count; FramebufferParams fb_params; Settings::TextureFilter filter; bool dump_textures; diff --git a/src/video_core/rasterizer_cache/surface_params.cpp b/src/video_core/rasterizer_cache/surface_params.cpp index 0ce956972..60985cdcb 100644 --- a/src/video_core/rasterizer_cache/surface_params.cpp +++ b/src/video_core/rasterizer_cache/surface_params.cpp @@ -1,6 +1,6 @@ -// Copyright 2022 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Copyright 2026 Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #include "common/alignment.h" #include "video_core/rasterizer_cache/surface_params.h" @@ -219,12 +219,13 @@ u32 SurfaceParams::LevelOf(PAddr level_addr) const { return level; } -std::string SurfaceParams::DebugName(bool scaled, bool custom) const noexcept { +std::string SurfaceParams::DebugName(bool scaled, bool custom, u8 sample_count) const noexcept { const u32 scaled_width = scaled ? GetScaledWidth() : width; const u32 scaled_height = scaled ? GetScaledHeight() : height; - return fmt::format("Surface: {}x{} {} {} levels from {:#x} to {:#x} ({}{})", scaled_width, - scaled_height, PixelFormatAsString(pixel_format), levels, addr, end, - custom ? "custom," : "", scaled ? "scaled" : "unscaled"); + return fmt::format("Surface: {}x{} {} samples {} levels from {:#x} to {:#x} ({}{})", + scaled_width, scaled_height, PixelFormatAsString(pixel_format), + static_cast(sample_count), levels, addr, end, custom ? "custom," : "", + scaled ? "scaled" : "unscaled"); } bool SurfaceParams::operator==(const SurfaceParams& other) const noexcept { diff --git a/src/video_core/rasterizer_cache/surface_params.h b/src/video_core/rasterizer_cache/surface_params.h index 280bb7ebf..d04e6af82 100644 --- a/src/video_core/rasterizer_cache/surface_params.h +++ b/src/video_core/rasterizer_cache/surface_params.h @@ -1,6 +1,6 @@ -// Copyright 2022 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Copyright 2026 Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #pragma once @@ -51,7 +51,7 @@ public: u32 LevelOf(PAddr addr) const; /// Returns a string identifier of the params object - std::string DebugName(bool scaled, bool custom = false) const noexcept; + std::string DebugName(bool scaled, bool custom = false, u8 sample_count = 1) const noexcept; bool operator==(const SurfaceParams& other) const noexcept; @@ -71,6 +71,10 @@ public: return height * res_scale; } + [[nodiscard]] u8 GetSampleCount() const noexcept { + return sample_count; + } + [[nodiscard]] Common::Rectangle GetRect(u32 level = 0) const noexcept { return {0, height >> level, width >> level, 0}; } @@ -104,6 +108,7 @@ public: u32 stride = 0; u32 levels = 1; u32 res_scale = 1; + u8 sample_count = 1; bool is_tiled = false; TextureType texture_type = TextureType::Texture2D; diff --git a/src/video_core/renderer_base.cpp b/src/video_core/renderer_base.cpp index 0c7a4c94e..102577b65 100644 --- a/src/video_core/renderer_base.cpp +++ b/src/video_core/renderer_base.cpp @@ -30,6 +30,10 @@ u32 RendererBase::GetResolutionScaleFactor() { : render_window.GetFramebufferLayout().GetScalingRatio(); } +u8 RendererBase::GetSampleCount() const { + return Settings::GetAntiAliasingSampleCount(Settings::values.antialiasing.GetValue()); +} + void RendererBase::UpdateCurrentFramebufferLayout(bool is_portrait_mode) { const auto update_layout = [is_portrait_mode](Frontend::EmuWindow& window) { const Layout::FramebufferLayout& layout = window.GetFramebufferLayout(); diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h index c17788122..48d25f451 100644 --- a/src/video_core/renderer_base.h +++ b/src/video_core/renderer_base.h @@ -67,6 +67,9 @@ public: /// Returns the resolution scale factor relative to the native 3DS screen resolution u32 GetResolutionScaleFactor(); + /// Returns the MSAA sample count + u8 GetSampleCount() const; + /// Updates the framebuffer layout of the contained render window handle. void UpdateCurrentFramebufferLayout(bool is_portrait_mode = {}); diff --git a/src/video_core/renderer_opengl/gl_blit_helper.cpp b/src/video_core/renderer_opengl/gl_blit_helper.cpp index 18eb83d73..6f3de4eca 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.cpp +++ b/src/video_core/renderer_opengl/gl_blit_helper.cpp @@ -1,6 +1,6 @@ // Copyright 2023-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #include "common/scope_exit.h" #include "common/settings.h" @@ -11,7 +11,9 @@ #include "video_core/renderer_opengl/gl_texture_runtime.h" #include "video_core/host_shaders/format_reinterpreter/d24s8_to_rgba8_frag.h" +#include "video_core/host_shaders/format_reinterpreter/d24s8_to_rgba8_ms_frag.h" #include "video_core/host_shaders/format_reinterpreter/rgba4_to_rgb5a1_frag.h" +#include "video_core/host_shaders/format_reinterpreter/rgba4_to_rgb5a1_ms_frag.h" #include "video_core/host_shaders/full_screen_triangle_vert.h" #include "video_core/host_shaders/texture_filtering/bicubic_frag.h" #include "video_core/host_shaders/texture_filtering/mmpx_frag.h" @@ -65,8 +67,13 @@ BlitHelper::BlitHelper(const Driver& driver_) gradient_y_program{CreateProgram(HostShaders::Y_GRADIENT_FRAG, "Y_GRADIENT_FRAG")}, refine_program{CreateProgram(HostShaders::REFINE_FRAG, "REFINE_FRAG")}, d24s8_to_rgba8{CreateProgram(HostShaders::D24S8_TO_RGBA8_FRAG, "D24S8_TO_RGBA8_FRAG")}, - rgba4_to_rgb5a1{CreateProgram(HostShaders::RGBA4_TO_RGB5A1_FRAG, "RGBA4_TO_RGB5A1_FRAG")} { + d24s8_to_rgba8_ms{ + CreateProgram(HostShaders::D24S8_TO_RGBA8_MS_FRAG, "D24S8_TO_RGBA8_MS_FRAG")}, + rgba4_to_rgb5a1{CreateProgram(HostShaders::RGBA4_TO_RGB5A1_FRAG, "RGBA4_TO_RGB5A1_FRAG")}, + rgba4_to_rgb5a1_ms{ + CreateProgram(HostShaders::RGBA4_TO_RGB5A1_MS_FRAG, "RGBA4_TO_RGB5A1_MS_FRAG")} { vao.Create(); + read_fbo.Create(); draw_fbo.Create(); state.draw.vertex_array = vao.handle; for (u32 i = 0; i < 3; i++) { @@ -87,49 +94,63 @@ bool BlitHelper::ConvertDS24S8ToRGBA8(Surface& source, Surface& dest, OpenGLState prev_state = OpenGLState::GetCurState(); SCOPE_EXIT({ prev_state.Apply(); }); - state.texture_units[0].texture_2d = source.Handle(); - state.texture_units[0].target = GL_TEXTURE_2D; + const bool multisample = (source.sample_count > 1) && (dest.sample_count > 1); + const GLuint textarget = multisample ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D; + + state.texture_units[0].texture_2d = source.Handle(multisample ? 3 : 1); + state.texture_units[0].target = textarget; state.texture_units[0].sampler = 0; state.texture_units[1].sampler = 0; if (use_texture_view) { temp_tex.Create(); glActiveTexture(GL_TEXTURE1); - glTextureView(temp_tex.handle, GL_TEXTURE_2D, source.Handle(), GL_DEPTH24_STENCIL8, 0, 1, 0, - 1); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureView(temp_tex.handle, textarget, source.Handle(multisample ? 3 : 1), + GL_DEPTH24_STENCIL8, 0, 1, 0, 1); + if (!multisample) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + } } else if (copy.extent.width > temp_extent.width || copy.extent.height > temp_extent.height) { temp_extent = copy.extent; temp_tex.Release(); temp_tex.Create(); state.texture_units[1].texture_2d = temp_tex.handle; - state.texture_units[1].target = GL_TEXTURE_2D; + state.texture_units[1].target = textarget; state.Apply(); glActiveTexture(GL_TEXTURE1); - glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, temp_extent.width, - temp_extent.height); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + + if (multisample) { + glTexStorage2DMultisample(textarget, source.sample_count, GL_DEPTH24_STENCIL8, + temp_extent.width, temp_extent.height, true); + + } else { + glTexStorage2D(textarget, 1, GL_DEPTH24_STENCIL8, temp_extent.width, + temp_extent.height); + glTexParameteri(textarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(textarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + } } state.texture_units[1].texture_2d = temp_tex.handle; - state.texture_units[1].target = GL_TEXTURE_2D; + state.texture_units[1].target = textarget; state.Apply(); glActiveTexture(GL_TEXTURE1); if (!use_texture_view) { - glCopyImageSubData(source.Handle(), GL_TEXTURE_2D, 0, copy.src_offset.x, copy.src_offset.y, - 0, temp_tex.handle, GL_TEXTURE_2D, 0, copy.src_offset.x, + glCopyImageSubData(source.Handle(multisample ? 3 : 1), textarget, 0, copy.src_offset.x, + copy.src_offset.y, 0, temp_tex.handle, textarget, 0, copy.src_offset.x, copy.src_offset.y, 0, copy.extent.width, copy.extent.height, 1); } - glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); + glTexParameteri(textarget, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); const Common::Rectangle src_rect{copy.src_offset.x, copy.src_offset.y + copy.extent.height, copy.src_offset.x + copy.extent.width, copy.src_offset.x}; const Common::Rectangle dst_rect{copy.dst_offset.x, copy.dst_offset.y + copy.extent.height, copy.dst_offset.x + copy.extent.width, copy.dst_offset.x}; - SetParams(d24s8_to_rgba8, source.RealExtent(), src_rect); - Draw(d24s8_to_rgba8, dest.Handle(), draw_fbo.handle, 0, dst_rect); + + OGLProgram& blit_program = multisample ? d24s8_to_rgba8_ms : d24s8_to_rgba8; + SetParams(blit_program, source.RealExtent(), src_rect); + Draw(blit_program, dest.Handle(multisample ? 3 : 1), draw_fbo.handle, 0, dst_rect, multisample); if (use_texture_view) { temp_tex.Release(); @@ -139,6 +160,11 @@ bool BlitHelper::ConvertDS24S8ToRGBA8(Surface& source, Surface& dest, state.texture_units[0].sampler = linear_sampler.handle; state.texture_units[1].sampler = linear_sampler.handle; + if (multisample) { + // Resolve the destination image if needed + ResolveTexture(dest, copy.dst_level, copy.dst_layer); + } + return true; } @@ -147,19 +173,48 @@ bool BlitHelper::ConvertRGBA4ToRGB5A1(Surface& source, Surface& dest, OpenGLState prev_state = OpenGLState::GetCurState(); SCOPE_EXIT({ prev_state.Apply(); }); - state.texture_units[0].texture_2d = source.Handle(); - state.texture_units[0].target = GL_TEXTURE_2D; + const bool multisample = (source.sample_count > 1) && (dest.sample_count > 1); + + state.texture_units[0].texture_2d = source.Handle(multisample ? 3 : 1); const Common::Rectangle src_rect{copy.src_offset.x, copy.src_offset.y + copy.extent.height, copy.src_offset.x + copy.extent.width, copy.src_offset.x}; const Common::Rectangle dst_rect{copy.dst_offset.x, copy.dst_offset.y + copy.extent.height, copy.dst_offset.x + copy.extent.width, copy.dst_offset.x}; - SetParams(rgba4_to_rgb5a1, source.RealExtent(), src_rect); - Draw(rgba4_to_rgb5a1, dest.Handle(), draw_fbo.handle, 0, dst_rect); + + OGLProgram& blit_program = multisample ? rgba4_to_rgb5a1_ms : rgba4_to_rgb5a1; + SetParams(blit_program, source.RealExtent(), src_rect); + Draw(blit_program, dest.Handle(multisample ? 3 : 1), draw_fbo.handle, 0, dst_rect, multisample); + + if (multisample) { + // Resolve the destination image if needed + ResolveTexture(dest, copy.dst_level, copy.dst_layer); + } return true; } +void BlitHelper::ResolveTexture(Surface& surface, u32 level, u32 layer) { + OpenGLState prev_state = OpenGLState::GetCurState(); + SCOPE_EXIT({ prev_state.Apply(); }); + + state.draw.read_framebuffer = read_fbo.handle; + state.draw.draw_framebuffer = draw_fbo.handle; + state.texture_units[0].texture_2d = 0; + state.texture_units[1].texture_2d = 0; + state.texture_units[2].texture_2d = 0; + state.Apply(); + + surface.Attach(GL_READ_FRAMEBUFFER, level, layer, 3); + surface.Attach(GL_DRAW_FRAMEBUFFER, level, layer, 1); + const GLbitfield buffer_mask = surface.type == SurfaceType::Depth ? GL_DEPTH_BUFFER_BIT + : surface.type == SurfaceType::DepthStencil + ? (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) + : GL_COLOR_BUFFER_BIT; + glBlitFramebuffer(0, 0, surface.GetScaledWidth(), surface.GetScaledHeight(), 0, 0, + surface.GetScaledWidth(), surface.GetScaledHeight(), buffer_mask, GL_NEAREST); +} + bool BlitHelper::Filter(Surface& surface, const VideoCore::TextureBlit& blit) { const auto filter = Settings::values.texture_filter.GetValue(); const bool is_depth = @@ -303,7 +358,7 @@ void BlitHelper::SetParams(OGLProgram& program, const VideoCore::Extent& src_ext } void BlitHelper::Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 dst_level, - Common::Rectangle dst_rect) { + Common::Rectangle dst_rect, bool multisample) { state.draw.draw_framebuffer = dst_fbo; state.draw.shader_program = program.handle; state.viewport.x = dst_rect.left; @@ -312,9 +367,11 @@ void BlitHelper::Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 d state.viewport.height = dst_rect.GetHeight(); state.Apply(); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, + const GLuint textarget = multisample ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D; + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, textarget, dst_tex, dst_level); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, textarget, 0, 0); glDrawArrays(GL_TRIANGLES, 0, 3); } diff --git a/src/video_core/renderer_opengl/gl_blit_helper.h b/src/video_core/renderer_opengl/gl_blit_helper.h index 11cf97f1a..62fcb8e0c 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.h +++ b/src/video_core/renderer_opengl/gl_blit_helper.h @@ -1,6 +1,6 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Copyright 2023-2026 Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #pragma once @@ -31,6 +31,8 @@ public: bool ConvertRGBA4ToRGB5A1(Surface& source, Surface& dest, const VideoCore::TextureCopy& copy); + void ResolveTexture(Surface& surface, u32 level = 0, u32 layer = 0); + private: void FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit); void FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit); @@ -41,12 +43,13 @@ private: void SetParams(OGLProgram& program, const VideoCore::Extent& src_extent, Common::Rectangle src_rect); void Draw(OGLProgram& program, GLuint dst_tex, GLuint dst_fbo, u32 dst_level, - Common::Rectangle dst_rect); + Common::Rectangle dst_rect, bool multisample = false); private: const Driver& driver; OGLVertexArray vao; OpenGLState state; + OGLFramebuffer read_fbo; OGLFramebuffer draw_fbo; OGLSampler linear_sampler; OGLSampler nearest_sampler; @@ -59,7 +62,9 @@ private: OGLProgram gradient_y_program; OGLProgram refine_program; OGLProgram d24s8_to_rgba8; + OGLProgram d24s8_to_rgba8_ms; OGLProgram rgba4_to_rgb5a1; + OGLProgram rgba4_to_rgb5a1_ms; OGLTexture temp_tex; VideoCore::Extent temp_extent{}; diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 3ceb130db..4cc7777f3 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -164,6 +164,10 @@ RasterizerOpenGL::RasterizerOpenGL(Memory::MemorySystem& memory, Pica::PicaCore& glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer.GetHandle()); glEnable(GL_BLEND); + + glEnable(GL_MULTISAMPLE); + glEnable(GL_SAMPLE_SHADING); + glMinSampleShading(1.0f); } RasterizerOpenGL::~RasterizerOpenGL() = default; @@ -662,6 +666,22 @@ bool RasterizerOpenGL::Draw(bool accelerate, bool is_indexed) { } } + // Resolve after drawing, slow initial approach to ensure the MSAA and non-MSAA buffers are + // always in sync + if (framebuffer->color_id != VideoCore::SurfaceId{}) { + Surface& color_surface = res_cache.GetSurface(framebuffer->color_id); + if (color_surface.GetSampleCount() > 1) { + runtime.ResolveTexture(color_surface, framebuffer->color_level); + } + } + + if (framebuffer->depth_id != VideoCore::SurfaceId{}) { + Surface& depth_surface = res_cache.GetSurface(framebuffer->depth_id); + if (depth_surface.GetSampleCount() > 1) { + runtime.ResolveTexture(depth_surface, framebuffer->depth_level); + } + } + vertex_batch.clear(); if (shadow_rendering) { @@ -1025,7 +1045,8 @@ void RasterizerOpenGL::SyncAndUploadLUTs() { } void RasterizerOpenGL::UploadUniforms(bool accelerate_draw) { - // glBindBufferRange also changes the generic buffer binding point, so we sync the state first. + // glBindBufferRange also changes the generic buffer binding point, so we sync the state + // first. state.draw.uniform_buffer = uniform_buffer.GetHandle(); state.Apply(); diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index d5683e3bf..a1bec9bb1 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -92,20 +92,27 @@ static constexpr std::array CUSTOM_TUPLES = {{ return 0; } -[[nodiscard]] OGLTexture MakeHandle(GLenum target, u32 width, u32 height, u32 levels, +[[nodiscard]] OGLTexture MakeHandle(GLenum target, u32 width, u32 height, u32 levels, u32 samples, const FormatTuple& tuple, std::string_view debug_name = "") { OGLTexture texture{}; texture.Create(); - glBindTexture(target, texture.handle); - glTexStorage2D(target, levels, tuple.internal_format, width, height); + if (samples > 1) { + ASSERT(target == GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture.handle); + glTexStorage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, tuple.internal_format, width, + height, true); + } else { + glBindTexture(target, texture.handle); + glTexStorage2D(target, levels, tuple.internal_format, width, height); + } glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); if (!debug_name.empty()) { - glObjectLabel(GL_TEXTURE, texture.handle, -1, debug_name.data()); + glObjectLabel(GL_TEXTURE, texture.handle, debug_name.size(), debug_name.data()); } return texture; @@ -231,6 +238,14 @@ bool TextureRuntime::ClearTextureWithoutFbo(Surface& surface, clear.texture_rect.bottom, clear.texture_layer, clear.texture_rect.GetWidth(), clear.texture_rect.GetHeight(), 1, format, type, &clear.value); + + if (surface.sample_count > 1) { + // Clear MSAA too + glClearTexSubImage(surface.Handle(3), clear.texture_level, clear.texture_rect.left, + clear.texture_rect.bottom, clear.texture_layer, + clear.texture_rect.GetWidth(), clear.texture_rect.GetHeight(), 1, format, + type, &clear.value); + } return true; } @@ -252,31 +267,40 @@ void TextureRuntime::ClearTexture(Surface& surface, const VideoCore::TextureClea state.draw.draw_framebuffer = draw_fbos[FboIndex(surface.type)].handle; state.Apply(); - surface.Attach(GL_DRAW_FRAMEBUFFER, clear.texture_level, clear.texture_layer); + const auto ClearBuffer = [&surface, &state, &clear]() { + switch (surface.type) { + case SurfaceType::Color: + case SurfaceType::Texture: + state.color_mask.red_enabled = true; + state.color_mask.green_enabled = true; + state.color_mask.blue_enabled = true; + state.color_mask.alpha_enabled = true; + state.Apply(); + glClearBufferfv(GL_COLOR, 0, clear.value.color.AsArray()); + break; + case SurfaceType::Depth: + state.depth.write_mask = GL_TRUE; + state.Apply(); + glClearBufferfv(GL_DEPTH, 0, &clear.value.depth); + break; + case SurfaceType::DepthStencil: + state.depth.write_mask = GL_TRUE; + state.stencil.write_mask = -1; + state.Apply(); + glClearBufferfi(GL_DEPTH_STENCIL, 0, clear.value.depth, clear.value.stencil); + break; + default: + UNREACHABLE_MSG("Unknown surface type {}", surface.type); + } + }; - switch (surface.type) { - case SurfaceType::Color: - case SurfaceType::Texture: - state.color_mask.red_enabled = true; - state.color_mask.green_enabled = true; - state.color_mask.blue_enabled = true; - state.color_mask.alpha_enabled = true; - state.Apply(); - glClearBufferfv(GL_COLOR, 0, clear.value.color.AsArray()); - break; - case SurfaceType::Depth: - state.depth.write_mask = GL_TRUE; - state.Apply(); - glClearBufferfv(GL_DEPTH, 0, &clear.value.depth); - break; - case SurfaceType::DepthStencil: - state.depth.write_mask = GL_TRUE; - state.stencil.write_mask = -1; - state.Apply(); - glClearBufferfi(GL_DEPTH_STENCIL, 0, clear.value.depth, clear.value.stencil); - break; - default: - UNREACHABLE_MSG("Unknown surface type {}", surface.type); + surface.Attach(GL_DRAW_FRAMEBUFFER, clear.texture_level, clear.texture_layer); + ClearBuffer(); + + if (surface.GetSampleCount() > 1) { + // Clear MSAA too + surface.Attach(GL_DRAW_FRAMEBUFFER, clear.texture_level, clear.texture_layer, 3); + ClearBuffer(); } } @@ -304,6 +328,15 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, const VideoCore::TextureBlit& blit) { const DebugScope scope(*this, Common::Vec4f{}, "TextureRuntime::BlitTextures"); + // Must resolve images first + // Todo(wunk): Add a "dirty" flag for msaa resolves to avoid redundant image resolves + if (source.sample_count > 1) { + blit_helper.ResolveTexture(source, blit.src_level, blit.src_layer); + } + if (dest.sample_count > 1) { + blit_helper.ResolveTexture(dest, blit.dst_level, blit.dst_layer); + } + OpenGLState state = OpenGLState::GetCurState(); state.scissor.enabled = false; state.draw.read_framebuffer = read_fbos[FboIndex(source.type)].handle; @@ -355,11 +388,16 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param const GLenum target = texture_type == VideoCore::TextureType::CubeMap ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D; - textures[0] = MakeHandle(target, width, height, levels, tuple, DebugName(false)); + textures[0] = MakeHandle(target, width, height, levels, 1, tuple, DebugName(false)); if (res_scale != 1) { - textures[1] = MakeHandle(target, GetScaledWidth(), GetScaledHeight(), levels, tuple, + textures[1] = MakeHandle(target, GetScaledWidth(), GetScaledHeight(), levels, 1, tuple, DebugName(true, false)); } + + if (sample_count > 1) { + textures[3] = MakeHandle(target, GetScaledWidth(), GetScaledHeight(), levels, sample_count, + tuple, DebugName(true, false, sample_count)); + } } Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface, @@ -377,15 +415,19 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface custom_format = mat->format; material = mat; - textures[0] = MakeHandle(target, mat->width, mat->height, levels, tuple, DebugName(false)); + textures[0] = MakeHandle(target, mat->width, mat->height, levels, 1, tuple, DebugName(false)); if (res_scale != 1) { - textures[1] = MakeHandle(target, mat->width, mat->height, levels, DEFAULT_TUPLE, + textures[1] = MakeHandle(target, mat->width, mat->height, levels, 1, DEFAULT_TUPLE, DebugName(true, true)); } const bool has_normal = mat->Map(MapType::Normal); if (has_normal) { textures[2] = - MakeHandle(target, mat->width, mat->height, levels, tuple, DebugName(true, true)); + MakeHandle(target, mat->width, mat->height, levels, 1, tuple, DebugName(true, true)); + } + if (sample_count > 1) { + textures[3] = MakeHandle(target, mat->width, mat->height, sample_count, levels, + DEFAULT_TUPLE, DebugName(true, true, sample_count)); } } @@ -402,8 +444,8 @@ GLuint Surface::CopyHandle() noexcept { const DebugScope scope(*runtime, Common::Vec4f{}, "Surface::CopyHandle"); if (!copy_texture.handle) { - copy_texture = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, tuple, - DebugName(true)); + copy_texture = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, 1, + tuple, DebugName(true)); } for (u32 level = 0; level < levels; level++) { @@ -571,47 +613,57 @@ bool Surface::DownloadWithoutFbo(const VideoCore::BufferTextureCopy& download, return false; } -void Surface::Attach(GLenum target, u32 level, u32 layer, bool scaled) { - const GLuint handle = Handle(static_cast(scaled)); - const GLenum textarget = texture_type == TextureType::CubeMap - ? GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer - : GL_TEXTURE_2D; +void Surface::Attach(GLenum target, u32 level, u32 layer, u32 handle) { + const GLuint gl_handle = Handle(handle); + GLenum textarget = texture_type == TextureType::CubeMap ? GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer + : GL_TEXTURE_2D; + + if (handle == 3 && sample_count > 1) { + ASSERT(texture_type == TextureType::Texture2D); + textarget = GL_TEXTURE_2D_MULTISAMPLE; + } switch (type) { case SurfaceType::Color: case SurfaceType::Texture: - glFramebufferTexture2D(target, GL_COLOR_ATTACHMENT0, textarget, handle, level); + glFramebufferTexture2D(target, GL_COLOR_ATTACHMENT0, textarget, gl_handle, level); break; case SurfaceType::Depth: - glFramebufferTexture2D(target, GL_DEPTH_ATTACHMENT, textarget, handle, level); + glFramebufferTexture2D(target, GL_DEPTH_ATTACHMENT, textarget, gl_handle, level); break; case SurfaceType::DepthStencil: - glFramebufferTexture2D(target, GL_DEPTH_STENCIL_ATTACHMENT, textarget, handle, level); + glFramebufferTexture2D(target, GL_DEPTH_STENCIL_ATTACHMENT, textarget, gl_handle, level); break; default: UNREACHABLE_MSG("Invalid surface type!"); } } -void Surface::ScaleUp(u32 new_scale) { - const DebugScope scope(*runtime, Common::Vec4f{}, "Surface::ScaleUp (NewScale:{})", new_scale); +void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { + const DebugScope scope(*runtime, Common::Vec4f{}, + "Surface::ScaleUp (NewScale:{} NewSamples:{})", new_scale, + new_sample_count); - if (res_scale == new_scale || new_scale == 1) { - return; + const bool res_scale_modified = res_scale != new_scale; + if (res_scale_modified && new_scale > 1) { + res_scale = new_scale; + textures[1] = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, 1, + tuple, DebugName(true)); + for (u32 level = 0; level < levels; level++) { + const VideoCore::TextureBlit blit = { + .src_level = level, + .dst_level = level, + .src_rect = GetRect(level), + .dst_rect = GetScaledRect(level), + }; + BlitScale(blit, true); + } } - res_scale = new_scale; - textures[1] = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, tuple, - DebugName(true)); - - for (u32 level = 0; level < levels; level++) { - const VideoCore::TextureBlit blit = { - .src_level = level, - .dst_level = level, - .src_rect = GetRect(level), - .dst_rect = GetScaledRect(level), - }; - BlitScale(blit, true); + if ((res_scale_modified || sample_count != new_sample_count) && new_sample_count > 1) { + sample_count = new_sample_count; + textures[3] = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, + sample_count, tuple, DebugName(true)); } } @@ -647,7 +699,8 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferParams& params, const Surface* color, const Surface* depth) : VideoCore::FramebufferParams{params}, - res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)} { + res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)}, + sample_count{color ? color->sample_count : (depth ? depth->sample_count : 1u)} { if (shadow_rendering && !color) { return; @@ -660,6 +713,15 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa attachments[1] = depth->Handle(); } + if (sample_count > 1) { + if (color) { + attachments[2] = color->Handle(3); + } + if (depth) { + attachments[3] = depth->Handle(3); + } + } + framebuffer.Create(); OpenGLState state = OpenGLState::GetCurState(); @@ -691,6 +753,27 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0); } + + if (sample_count > 1) { + if (color) { + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D_MULTISAMPLE, color ? color->Handle(3) : 0, + color_level); + } + if (depth) { + if (depth->pixel_format == PixelFormat::D24S8) { + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_TEXTURE_2D_MULTISAMPLE, depth->Handle(3), + depth_level); + } else { + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D_MULTISAMPLE, depth->Handle(3), + depth_level); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, + GL_TEXTURE_2D_MULTISAMPLE, 0, 0); + } + } + } } } diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.h b/src/video_core/renderer_opengl/gl_texture_runtime.h index dc2ef55f8..985fe18ab 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.h +++ b/src/video_core/renderer_opengl/gl_texture_runtime.h @@ -85,6 +85,11 @@ public: /// Generates mipmaps for all the available levels of the texture void GenerateMipmaps(Surface& surface); + /// Resolve a surface's MSAA texture into the surface's appropriate non-MSAA texture + void ResolveTexture(Surface& surface, u32 level = 0, u32 layer = 0) { + blit_helper.ResolveTexture(surface, level, layer); + } + private: /// Returns the OpenGL driver class const Driver& GetDriver() const { @@ -138,10 +143,10 @@ public: const VideoCore::StagingData& staging); /// Attaches a handle of surface to the specified framebuffer target - void Attach(GLenum target, u32 level, u32 layer, bool scaled = true); + void Attach(GLenum target, u32 level, u32 layer, u32 handle = 1); - /// Scales up the surface to match the new resolution scale. - void ScaleUp(u32 new_scale); + /// Scales up the surface to match the new resolution scale and sample-count. + void ScaleUp(u32 new_scale, u8 new_sample_count); /// Returns the bpp of the internal surface format u32 GetInternalBytesPerPixel() const; @@ -157,7 +162,7 @@ private: private: const Driver* driver; TextureRuntime* runtime; - std::array textures; + std::array textures; OGLTexture copy_texture; FormatTuple tuple; }; @@ -178,6 +183,10 @@ public: return res_scale; } + [[nodiscard]] u32 Samples() const noexcept { + return sample_count; + } + [[nodiscard]] GLuint Handle() const noexcept { return framebuffer.handle; } @@ -192,7 +201,8 @@ public: private: u32 res_scale{1}; - std::array attachments{}; + u32 sample_count{1}; + std::array attachments{}; OGLFramebuffer framebuffer; }; diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.cpp b/src/video_core/renderer_vulkan/vk_blit_helper.cpp index 77e88841e..7d4d564e2 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_helper.cpp @@ -1,6 +1,6 @@ // Copyright 2022-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #include "common/hash.h" #include "common/settings.h" @@ -14,6 +14,7 @@ #include "video_core/renderer_vulkan/vk_texture_runtime.h" #include "video_core/host_shaders/format_reinterpreter/vulkan_d24s8_to_rgba8_comp.h" +#include "video_core/host_shaders/format_reinterpreter/vulkan_d24s8_to_rgba8_ms_comp.h" #include "video_core/host_shaders/full_screen_triangle_vert.h" #include "video_core/host_shaders/vulkan_blit_depth_stencil_frag.h" #include "video_core/host_shaders/vulkan_depth_to_buffer_comp.h" @@ -248,6 +249,10 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, vk::ShaderStageFlagBits::eVertex, device)}, d24s8_to_rgba8_comp{Compile(HostShaders::VULKAN_D24S8_TO_RGBA8_COMP, vk::ShaderStageFlagBits::eCompute, device)}, +#if !defined(__APPLE__) + d24s8_to_rgba8_ms_comp{Compile(HostShaders::VULKAN_D24S8_TO_RGBA8_MS_COMP, + vk::ShaderStageFlagBits::eCompute, device)}, +#endif depth_to_buffer_comp{Compile(HostShaders::VULKAN_DEPTH_TO_BUFFER_COMP, vk::ShaderStageFlagBits::eCompute, device)}, blit_depth_stencil_frag{VK_NULL_HANDLE}, @@ -260,6 +265,10 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, mmpx_frag{Compile(HostShaders::MMPX_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, refine_frag{Compile(HostShaders::REFINE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, d24s8_to_rgba8_pipeline{MakeComputePipeline(d24s8_to_rgba8_comp, compute_pipeline_layout)}, +#if !defined(__APPLE__) + d24s8_to_rgba8_ms_pipeline{ + MakeComputePipeline(d24s8_to_rgba8_ms_comp, compute_pipeline_layout)}, +#endif depth_to_buffer_pipeline{ MakeComputePipeline(depth_to_buffer_comp, compute_buffer_pipeline_layout)}, depth_blit_pipeline{VK_NULL_HANDLE}, @@ -284,11 +293,17 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, "BlitHelper: three_textures_pipeline_layout"); SetObjectName(device, full_screen_vert, "BlitHelper: full_screen_vert"); SetObjectName(device, d24s8_to_rgba8_comp, "BlitHelper: d24s8_to_rgba8_comp"); +#if !defined(__APPLE__) + SetObjectName(device, d24s8_to_rgba8_ms_comp, "BlitHelper: d24s8_to_rgba8_ms_comp"); +#endif SetObjectName(device, depth_to_buffer_comp, "BlitHelper: depth_to_buffer_comp"); if (blit_depth_stencil_frag) { SetObjectName(device, blit_depth_stencil_frag, "BlitHelper: blit_depth_stencil_frag"); } SetObjectName(device, d24s8_to_rgba8_pipeline, "BlitHelper: d24s8_to_rgba8_pipeline"); +#if !defined(__APPLE__) + SetObjectName(device, d24s8_to_rgba8_ms_pipeline, "BlitHelper: d24s8_to_rgba8_ms_pipeline"); +#endif SetObjectName(device, depth_to_buffer_pipeline, "BlitHelper: depth_to_buffer_pipeline"); if (depth_blit_pipeline) { SetObjectName(device, depth_blit_pipeline, "BlitHelper: depth_blit_pipeline"); @@ -310,6 +325,9 @@ BlitHelper::~BlitHelper() { device.destroyPipelineLayout(three_textures_pipeline_layout); device.destroyShaderModule(full_screen_vert); device.destroyShaderModule(d24s8_to_rgba8_comp); +#if !defined(__APPLE__) + device.destroyShaderModule(d24s8_to_rgba8_ms_comp); +#endif device.destroyShaderModule(depth_to_buffer_comp); if (blit_depth_stencil_frag) { device.destroyShaderModule(blit_depth_stencil_frag); @@ -322,6 +340,9 @@ BlitHelper::~BlitHelper() { device.destroyShaderModule(refine_frag); device.destroyPipeline(depth_to_buffer_pipeline); device.destroyPipeline(d24s8_to_rgba8_pipeline); +#if !defined(__APPLE__) + device.destroyPipeline(d24s8_to_rgba8_ms_pipeline); +#endif device.destroyPipeline(depth_blit_pipeline); device.destroySampler(linear_sampler); device.destroySampler(nearest_sampler); @@ -401,16 +422,27 @@ bool BlitHelper::BlitDepthStencil(Surface& source, Surface& dest, bool BlitHelper::ConvertDS24S8ToRGBA8(Surface& source, Surface& dest, const VideoCore::TextureCopy& copy) { + + const bool multisample = (source.sample_count > 1) && (dest.sample_count > 1); + const Type src_type = multisample ? Type::MultiSampled : Type::Current; +#if !defined(__APPLE__) + const auto pipeline = multisample ? d24s8_to_rgba8_ms_pipeline : d24s8_to_rgba8_pipeline; +#else + const auto pipeline = d24s8_to_rgba8_pipeline; +#endif + const auto descriptor_set = compute_provider.Commit(); - update_queue.AddImageSampler(descriptor_set, 0, 0, source.DepthView(), VK_NULL_HANDLE, + update_queue.AddImageSampler(descriptor_set, 0, 0, source.ImageView(ViewType::Depth, src_type), + VK_NULL_HANDLE, vk::ImageLayout::eDepthStencilReadOnlyOptimal); + update_queue.AddImageSampler(descriptor_set, 1, 0, + source.ImageView(ViewType::Stencil, src_type), VK_NULL_HANDLE, vk::ImageLayout::eDepthStencilReadOnlyOptimal); - update_queue.AddImageSampler(descriptor_set, 1, 0, source.StencilView(), VK_NULL_HANDLE, - vk::ImageLayout::eDepthStencilReadOnlyOptimal); - update_queue.AddStorageImage(descriptor_set, 2, dest.ImageView()); + update_queue.AddStorageImage(descriptor_set, 2, dest.ImageView(ViewType::Sample, src_type)); renderpass_cache.EndRendering(); - scheduler.Record([this, descriptor_set, copy, src_image = source.Image(), - dst_image = dest.Image()](vk::CommandBuffer cmdbuf) { + + scheduler.Record([this, pipeline, descriptor_set, copy, src_image = source.Image(src_type), + dst_image = dest.Image(src_type)](vk::CommandBuffer cmdbuf) { const std::array pre_barriers = { vk::ImageMemoryBarrier{ .srcAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentWrite, @@ -488,7 +520,7 @@ bool BlitHelper::ConvertDS24S8ToRGBA8(Surface& source, Surface& dest, cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eCompute, compute_pipeline_layout, 0, descriptor_set, {}); - cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, d24s8_to_rgba8_pipeline); + cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline); const ComputeInfo info = { .src_offset = Common::Vec2i{static_cast(copy.src_offset.x), @@ -507,6 +539,11 @@ bool BlitHelper::ConvertDS24S8ToRGBA8(Surface& source, Surface& dest, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlagBits::eByRegion, {}, {}, post_barriers); }); + + if (multisample) { + // Resolve the destination image if needed + ResolveTexture(dest); + } return true; } @@ -585,6 +622,99 @@ bool BlitHelper::DepthToBuffer(Surface& source, vk::Buffer buffer, return true; } +void BlitHelper::ResolveTexture(Surface& surface) { + + scheduler.Record([width = surface.GetScaledWidth(), height = surface.GetScaledHeight(), + aspect = surface.Aspect(), access_flags = surface.AccessFlags(), + pipeline_state_flags = surface.PipelineStageFlags(), + msaa_image = surface.Image(Type::MultiSampled), + dest_image = surface.Image()](vk::CommandBuffer cmdbuf) { + const vk::ImageResolve resolve_area = { + .srcSubresource{ + .aspectMask = aspect, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .srcOffset = {}, + .dstSubresource{ + .aspectMask = aspect, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .dstOffset = {}, + .extent{ + .width = width, + .height = height, + .depth = 1, + }, + }; + + const vk::ImageSubresourceRange subresource_range = vk::ImageSubresourceRange{ + .aspectMask = aspect, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }; + + const std::array read_barriers = { + vk::ImageMemoryBarrier{ + .srcAccessMask = access_flags, + .dstAccessMask = vk::AccessFlagBits::eTransferRead, + .oldLayout = vk::ImageLayout::eGeneral, + .newLayout = vk::ImageLayout::eTransferSrcOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = msaa_image, + .subresourceRange = subresource_range, + }, + vk::ImageMemoryBarrier{ + .srcAccessMask = access_flags, + .dstAccessMask = vk::AccessFlagBits::eTransferWrite, + .oldLayout = vk::ImageLayout::eGeneral, + .newLayout = vk::ImageLayout::eTransferDstOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = dest_image, + .subresourceRange = subresource_range, + }, + }; + const std::array write_barriers = { + vk::ImageMemoryBarrier{ + .srcAccessMask = vk::AccessFlagBits::eTransferRead, + .dstAccessMask = access_flags, + .oldLayout = vk::ImageLayout::eTransferSrcOptimal, + .newLayout = vk::ImageLayout::eGeneral, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = msaa_image, + .subresourceRange = subresource_range, + }, + vk::ImageMemoryBarrier{ + .srcAccessMask = vk::AccessFlagBits::eTransferWrite, + .dstAccessMask = access_flags, + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eGeneral, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = dest_image, + .subresourceRange = subresource_range, + }, + }; + + cmdbuf.pipelineBarrier(pipeline_state_flags, vk::PipelineStageFlagBits::eTransfer, + vk::DependencyFlagBits::eByRegion, {}, {}, read_barriers); + + cmdbuf.resolveImage(msaa_image, vk::ImageLayout::eTransferSrcOptimal, dest_image, + vk::ImageLayout::eTransferDstOptimal, resolve_area); + + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, pipeline_state_flags, + vk::DependencyFlagBits::eByRegion, {}, {}, write_barriers); + }); +} + vk::Pipeline BlitHelper::MakeComputePipeline(vk::ShaderModule shader, vk::PipelineLayout layout) { const vk::ComputePipelineCreateInfo compute_info = { .stage = MakeStages(shader), diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.h b/src/video_core/renderer_vulkan/vk_blit_helper.h index a31e2290f..3020f614a 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.h +++ b/src/video_core/renderer_vulkan/vk_blit_helper.h @@ -1,6 +1,6 @@ // Copyright 2023-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #pragma once @@ -39,6 +39,8 @@ public: bool DepthToBuffer(Surface& source, vk::Buffer buffer, const VideoCore::BufferTextureCopy& copy); + void ResolveTexture(Surface& surface); + private: vk::Pipeline MakeComputePipeline(vk::ShaderModule shader, vk::PipelineLayout layout); vk::Pipeline MakeDepthStencilBlitPipeline(); @@ -83,6 +85,7 @@ private: vk::ShaderModule full_screen_vert; vk::ShaderModule d24s8_to_rgba8_comp; + vk::ShaderModule d24s8_to_rgba8_ms_comp; vk::ShaderModule depth_to_buffer_comp; vk::ShaderModule blit_depth_stencil_frag; vk::ShaderModule bicubic_frag; @@ -92,6 +95,7 @@ private: vk::ShaderModule refine_frag; vk::Pipeline d24s8_to_rgba8_pipeline; + vk::Pipeline d24s8_to_rgba8_ms_pipeline; vk::Pipeline depth_to_buffer_pipeline; vk::Pipeline depth_blit_pipeline; vk::Sampler linear_sampler; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 55633e41b..5cf8c83d9 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -163,8 +163,9 @@ bool GraphicsPipeline::Build(bool fail_on_compile_required) { }; const vk::PipelineMultisampleStateCreateInfo multisampling = { - .rasterizationSamples = vk::SampleCountFlagBits::e1, - .sampleShadingEnable = false, + .rasterizationSamples = vk::SampleCountFlagBits(info.state.attachments.sample_count), + .sampleShadingEnable = true, + .minSampleShading = 1.0f, }; const auto& blending = info.state.blending; @@ -296,7 +297,8 @@ bool GraphicsPipeline::Build(bool fail_on_compile_required) { .pDynamicState = &dynamic_info, .layout = pipeline_layout, .renderPass = renderpass_cache.GetRenderpass(info.state.attachments.color, - info.state.attachments.depth, false), + info.state.attachments.depth, false, + info.state.attachments.sample_count), }; if (fail_on_compile_required) { diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h index 1d310e400..b3d6793b0 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -217,6 +217,7 @@ static_assert(std::is_trivial_v); struct AttachmentInfo { VideoCore::PixelFormat color; VideoCore::PixelFormat depth; + u8 sample_count; static consteval u64 StructHash() { constexpr u64 STRUCT_VERSION = 0; @@ -228,7 +229,7 @@ struct AttachmentInfo { LAYOUT_HASH, // fields - FIELD_HASH(color), FIELD_HASH(depth)); + FIELD_HASH(color), FIELD_HASH(depth), FIELD_HASH(sample_count)); } }; static_assert(std::is_trivial_v); diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index d9697d163..077541cdb 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -1,6 +1,6 @@ // Copyright 2023-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #include #include @@ -421,7 +421,7 @@ bool Instance::CreateDevice() { return false; } - boost::container::static_vector enabled_extensions; + boost::container::static_vector enabled_extensions; const auto add_extension = [&](std::string_view extension, bool blacklist = false, std::string_view reason = "") -> bool { const auto result = @@ -450,6 +450,8 @@ bool Instance::CreateDevice() { add_extension(VK_KHR_SWAPCHAIN_EXTENSION_NAME); image_format_list = add_extension(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); + create_renderpass2 = add_extension(VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME); + depth_stencil_resolve = add_extension(VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME); shader_stencil_export = add_extension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); external_memory_host = add_extension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); tooling_info = add_extension(VK_EXT_TOOLING_INFO_EXTENSION_NAME, is_moltenvk, @@ -521,9 +523,11 @@ bool Instance::CreateDevice() { .features{ .robustBufferAccess = features.robustBufferAccess, .geometryShader = features.geometryShader, + .sampleRateShading = features.sampleRateShading, .logicOp = features.logicOp, .samplerAnisotropy = features.samplerAnisotropy, .fragmentStoresAndAtomics = features.fragmentStoresAndAtomics, + .shaderStorageImageMultisample = features.shaderStorageImageMultisample, .shaderClipDistance = features.shaderClipDistance, }, }, diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 94ace3642..fedf4a05a 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -1,6 +1,6 @@ // Copyright 2022-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #pragma once @@ -256,6 +256,11 @@ public: return triangle_fan_supported; } + // Returns true when sampleRateShading, VK_KHR_create_renderpass2, VK_KHR_depth_stencil_resolve + bool IsMultiSampleSupported() const { + return features.sampleRateShading && create_renderpass2 && depth_stencil_resolve; + } + /// Returns true if dynamic indices can be used inside shaders. bool IsImageArrayDynamicIndexSupported() const { return features.shaderSampledImageArrayDynamicIndexing; @@ -336,6 +341,8 @@ protected: bool null_descriptor{}; bool fragment_shader_interlock{}; bool image_format_list{}; + bool create_renderpass2{}; + bool depth_stencil_resolve{}; bool pipeline_creation_cache_control{}; bool fragment_shader_barycentric{}; bool shader_stencil_export{}; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index b52b8d56f..6b753b83c 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -588,6 +588,7 @@ bool RasterizerVulkan::Draw(bool accelerate, bool is_indexed) { pipeline_info.state.attachments.color = framebuffer->Format(SurfaceType::Color); pipeline_info.state.attachments.depth = framebuffer->Format(SurfaceType::Depth); + pipeline_info.state.attachments.sample_count = framebuffer->Samples(); // Update scissor uniforms const auto [scissor_x1, scissor_y2, scissor_x2, scissor_y1] = fb_helper.Scissor(); diff --git a/src/video_core/renderer_vulkan/vk_render_manager.cpp b/src/video_core/renderer_vulkan/vk_render_manager.cpp index 5d2b4f6ae..4ac6928e5 100644 --- a/src/video_core/renderer_vulkan/vk_render_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_render_manager.cpp @@ -1,7 +1,9 @@ // Copyright 2024-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. +#include +#include #include "common/assert.h" #include "video_core/rasterizer_cache/pixel_format.h" #include "video_core/renderer_vulkan/vk_instance.h" @@ -37,7 +39,7 @@ void RenderManager::BeginRendering(const Framebuffer* framebuffer, .framebuffer = framebuffer->Handle(), .render_pass = framebuffer->RenderPass(), .render_area = render_area, - .clear = {}, + .clears = {}, .do_clear = false, }; images = framebuffer->Images(); @@ -58,8 +60,8 @@ void RenderManager::BeginRendering(const RenderPass& new_pass) { .renderPass = info.render_pass, .framebuffer = info.framebuffer, .renderArea = info.render_area, - .clearValueCount = info.do_clear ? 1u : 0u, - .pClearValues = &info.clear, + .clearValueCount = info.do_clear ? 2u : 0u, + .pClearValues = info.clears.data(), }; cmdbuf.beginRenderPass(renderpass_begin_info, vk::SubpassContents::eInline); }); @@ -77,7 +79,7 @@ void RenderManager::EndRendering() { u32 num_barriers = 0; vk::PipelineStageFlags pipeline_flags{}; vk::AccessFlags src_access_flags{}; - std::array barriers; + std::array barriers; for (u32 i = 0; i < images.size(); i++) { if (!images[i]) { continue; @@ -138,7 +140,8 @@ void RenderManager::EndRendering() { } vk::RenderPass RenderManager::GetRenderpass(VideoCore::PixelFormat color, - VideoCore::PixelFormat depth, bool is_clear) { + VideoCore::PixelFormat depth, bool is_clear, + u8 sample_count) { std::scoped_lock lock{cache_mutex}; const u32 color_index = @@ -151,13 +154,23 @@ vk::RenderPass RenderManager::GetRenderpass(VideoCore::PixelFormat color, ASSERT_MSG(color_index <= NumColorFormats && depth_index <= NumDepthFormats, "Invalid color index {} and/or depth_index {}", color_index, depth_index); - vk::UniqueRenderPass& renderpass = cached_renderpasses[color_index][depth_index][is_clear]; + ASSERT_MSG(sample_count && std::has_single_bit(sample_count) && sample_count <= MaxSamples, + "Invalid sample count {}", static_cast(sample_count)); + + const u32 samples_index = static_cast(std::bit_width(sample_count) - 1); + + vk::UniqueRenderPass& renderpass = + cached_renderpasses[color_index][depth_index][samples_index][is_clear]; if (!renderpass) { const vk::Format color_format = instance.GetTraits(color).native; const vk::Format depth_format = instance.GetTraits(depth).native; const vk::AttachmentLoadOp load_op = is_clear ? vk::AttachmentLoadOp::eClear : vk::AttachmentLoadOp::eLoad; - renderpass = CreateRenderPass(color_format, depth_format, load_op); + + renderpass = (sample_count > 1) + ? CreateRenderPassMSAA(color_format, depth_format, load_op, + static_cast(sample_count)) + : CreateRenderPass(color_format, depth_format, load_op); } return *renderpass; @@ -165,27 +178,27 @@ vk::RenderPass RenderManager::GetRenderpass(VideoCore::PixelFormat color, vk::UniqueRenderPass RenderManager::CreateRenderPass(vk::Format color, vk::Format depth, vk::AttachmentLoadOp load_op) const { - u32 attachment_count = 0; - std::array attachments; + boost::container::static_vector attachments{}; bool use_color = false; vk::AttachmentReference color_attachment_ref{}; bool use_depth = false; vk::AttachmentReference depth_attachment_ref{}; if (color != vk::Format::eUndefined) { - attachments[attachment_count] = vk::AttachmentDescription{ + attachments.emplace_back(vk::AttachmentDescription{ .format = color, + .samples = vk::SampleCountFlagBits::e1, .loadOp = load_op, .storeOp = vk::AttachmentStoreOp::eStore, .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, .initialLayout = vk::ImageLayout::eGeneral, .finalLayout = vk::ImageLayout::eGeneral, - }; + }); color_attachment_ref = vk::AttachmentReference{ - .attachment = attachment_count++, + .attachment = static_cast(attachments.size() - 1), .layout = vk::ImageLayout::eGeneral, }; @@ -193,18 +206,19 @@ vk::UniqueRenderPass RenderManager::CreateRenderPass(vk::Format color, vk::Forma } if (depth != vk::Format::eUndefined) { - attachments[attachment_count] = vk::AttachmentDescription{ + attachments.emplace_back(vk::AttachmentDescription{ .format = depth, + .samples = vk::SampleCountFlagBits::e1, .loadOp = load_op, .storeOp = vk::AttachmentStoreOp::eStore, .stencilLoadOp = load_op, .stencilStoreOp = vk::AttachmentStoreOp::eStore, .initialLayout = vk::ImageLayout::eGeneral, .finalLayout = vk::ImageLayout::eGeneral, - }; + }); depth_attachment_ref = vk::AttachmentReference{ - .attachment = attachment_count++, + .attachment = static_cast(attachments.size() - 1), .layout = vk::ImageLayout::eGeneral, }; @@ -217,12 +231,11 @@ vk::UniqueRenderPass RenderManager::CreateRenderPass(vk::Format color, vk::Forma .pInputAttachments = nullptr, .colorAttachmentCount = use_color ? 1u : 0u, .pColorAttachments = &color_attachment_ref, - .pResolveAttachments = 0, .pDepthStencilAttachment = use_depth ? &depth_attachment_ref : nullptr, }; const vk::RenderPassCreateInfo renderpass_info = { - .attachmentCount = attachment_count, + .attachmentCount = static_cast(attachments.size()), .pAttachments = attachments.data(), .subpassCount = 1, .pSubpasses = &subpass, @@ -233,4 +246,132 @@ vk::UniqueRenderPass RenderManager::CreateRenderPass(vk::Format color, vk::Forma return instance.GetDevice().createRenderPassUnique(renderpass_info); } +vk::UniqueRenderPass RenderManager::CreateRenderPassMSAA( + vk::Format color, vk::Format depth, vk::AttachmentLoadOp load_op, + vk::SampleCountFlagBits sample_count) const { + boost::container::static_vector attachments{}; + + vk::AttachmentReference2 color_resolve_attachment = {.attachment = VK_ATTACHMENT_UNUSED}; + vk::AttachmentReference2 depth_resolve_attachment = {.attachment = VK_ATTACHMENT_UNUSED}; + + bool use_color = false; + vk::AttachmentReference2 color_attachment_ref{}; + bool use_depth = false; + vk::AttachmentReference2 depth_attachment_ref{}; + + if (color != vk::Format::eUndefined) { + attachments.emplace_back(vk::AttachmentDescription2{ + .format = color, + .samples = vk::SampleCountFlagBits::e1, + .loadOp = load_op, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eGeneral, + .finalLayout = vk::ImageLayout::eGeneral, + }); + + color_attachment_ref = vk::AttachmentReference2{ + .attachment = static_cast(attachments.size() - 1), + .layout = vk::ImageLayout::eGeneral, + .aspectMask = vk::ImageAspectFlagBits::eColor, + }; + + use_color = true; + } + + if (depth != vk::Format::eUndefined) { + attachments.emplace_back(vk::AttachmentDescription2{ + .format = depth, + .samples = vk::SampleCountFlagBits::e1, + .loadOp = load_op, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = load_op, + .stencilStoreOp = vk::AttachmentStoreOp::eStore, + .initialLayout = vk::ImageLayout::eGeneral, + .finalLayout = vk::ImageLayout::eGeneral, + }); + + depth_attachment_ref = vk::AttachmentReference2{ + .attachment = static_cast(attachments.size() - 1), + .layout = vk::ImageLayout::eGeneral, + .aspectMask = vk::ImageAspectFlagBits::eDepth, + }; + + use_depth = true; + } + + // In the case of MSAA, each attachment gets an additional MSAA attachment that now becomes the + // main attachment and the original attachments now get resolved into + if (sample_count > vk::SampleCountFlagBits::e1) { + if (color != vk::Format::eUndefined) { + attachments.emplace_back(vk::AttachmentDescription2{ + .format = color, + .samples = sample_count, + .loadOp = load_op, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eGeneral, + .finalLayout = vk::ImageLayout::eGeneral, + }); + + color_resolve_attachment = color_attachment_ref; + + color_attachment_ref = vk::AttachmentReference2{ + .attachment = static_cast(attachments.size() - 1), + .layout = vk::ImageLayout::eGeneral, + }; + } + + if (depth != vk::Format::eUndefined) { + attachments.emplace_back(vk::AttachmentDescription2{ + .format = depth, + .samples = sample_count, + .loadOp = load_op, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = load_op, + .stencilStoreOp = vk::AttachmentStoreOp::eStore, + .initialLayout = vk::ImageLayout::eGeneral, + .finalLayout = vk::ImageLayout::eGeneral, + }); + + depth_resolve_attachment = depth_attachment_ref; + + depth_attachment_ref = vk::AttachmentReference2{ + .attachment = static_cast(attachments.size() - 1), + .layout = vk::ImageLayout::eGeneral, + }; + } + } + + const vk::StructureChain + subpass = { + vk::SubpassDescription2{ + .pipelineBindPoint = vk::PipelineBindPoint::eGraphics, + .inputAttachmentCount = 0, + .pInputAttachments = nullptr, + .colorAttachmentCount = use_color ? 1u : 0u, + .pColorAttachments = &color_attachment_ref, + .pResolveAttachments = &color_resolve_attachment, + .pDepthStencilAttachment = use_depth ? &depth_attachment_ref : nullptr, + }, + vk::SubpassDescriptionDepthStencilResolve{ + .depthResolveMode = vk::ResolveModeFlagBits::eSampleZero, + .stencilResolveMode = vk::ResolveModeFlagBits::eSampleZero, + .pDepthStencilResolveAttachment = &depth_resolve_attachment}, + }; + + const vk::RenderPassCreateInfo2 renderpass_info = { + .attachmentCount = static_cast(attachments.size()), + .pAttachments = attachments.data(), + .subpassCount = 1, + .pSubpasses = &subpass.get(), + .dependencyCount = 0, + .pDependencies = nullptr, + }; + + return instance.GetDevice().createRenderPass2Unique(renderpass_info); +} + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_render_manager.h b/src/video_core/renderer_vulkan/vk_render_manager.h index cd8b81a1e..8d6270d87 100644 --- a/src/video_core/renderer_vulkan/vk_render_manager.h +++ b/src/video_core/renderer_vulkan/vk_render_manager.h @@ -1,9 +1,10 @@ // Copyright 2024-2026 Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the misc/licenses/gplv2.txt file included. +// Licensed under GPLv3 or any later version +// Refer to the LICENSE.txt file included. #pragma once +#include #include #include "common/math_util.h" @@ -23,20 +24,22 @@ struct RenderPass { vk::Framebuffer framebuffer; vk::RenderPass render_pass; vk::Rect2D render_area; - vk::ClearValue clear; + std::array clears; u32 do_clear; bool operator==(const RenderPass& other) const noexcept { return std::tie(framebuffer, render_pass, render_area, do_clear) == std::tie(other.framebuffer, other.render_pass, other.render_area, other.do_clear) && - std::memcmp(&clear, &other.clear, sizeof(vk::ClearValue)) == 0; + std::memcmp(&clears, &other.clears, sizeof(clears)) == 0; } }; class RenderManager { static constexpr u32 NumColorFormats = static_cast(VideoCore::PixelFormat::NumColorFormat); static constexpr u32 NumDepthFormats = static_cast(VideoCore::PixelFormat::NumDepthFormat); + static constexpr size_t MaxSamples = 16; + static_assert(std::has_single_bit(MaxSamples)); public: explicit RenderManager(const Instance& instance, Scheduler& scheduler); @@ -53,20 +56,26 @@ public: /// Returns the renderpass associated with the color-depth format pair vk::RenderPass GetRenderpass(VideoCore::PixelFormat color, VideoCore::PixelFormat depth, - bool is_clear); + bool is_clear, u8 sample_count = 1); private: /// Creates a renderpass configured appropriately and stores it in cached_renderpasses vk::UniqueRenderPass CreateRenderPass(vk::Format color, vk::Format depth, vk::AttachmentLoadOp load_op) const; + /// Creates an MSAA renderpass configured appropriately and stores it in cached_renderpasses + vk::UniqueRenderPass CreateRenderPassMSAA(vk::Format color, vk::Format depth, + vk::AttachmentLoadOp load_op, + vk::SampleCountFlagBits sample_count) const; + private: const Instance& instance; Scheduler& scheduler; - vk::UniqueRenderPass cached_renderpasses[NumColorFormats + 1][NumDepthFormats + 1][2]; + vk::UniqueRenderPass cached_renderpasses[NumColorFormats + 1][NumDepthFormats + 1] + [std::bit_width(MaxSamples)][2]; std::mutex cache_mutex; - std::array images; - std::array aspects; + std::array images; + std::array aspects; bool shadow_rendering{}; RenderPass pass{}; u32 num_draws{}; diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h index 0837e2109..568e9bca7 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h @@ -145,13 +145,13 @@ private: static_assert(sizeof(GSConfigEntry) == 48); struct PLConfigEntry { - static constexpr u8 EXPECTED_VERSION = 0; + static constexpr u8 EXPECTED_VERSION = 1; u64 version; // Surprise tool that can help us later StaticPipelineInfo pl_info; }; - static_assert(sizeof(PLConfigEntry) == 152); + static_assert(sizeof(PLConfigEntry) == 160); class CacheFile; class CacheEntry { diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 5ae84bdce..78df7d960 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -147,6 +147,11 @@ void MakeInitBarriers(vk::ImageAspectFlags aspect, u32 num_images, } } +void MakeInitBarriers(vk::ImageAspectFlags aspect, std::span images, + std::span out_barriers) { + MakeInitBarriers(aspect, images.size(), images, out_barriers); +} + vk::ImageSubresourceRange MakeSubresourceRange(vk::ImageAspectFlags aspect, u32 level = 0, u32 levels = 1, u32 layer = 0) { return vk::ImageSubresourceRange{ @@ -164,8 +169,8 @@ constexpr u64 DOWNLOAD_BUFFER_SIZE = 16_MiB; } // Anonymous namespace void Handle::Create(u32 width, u32 height, u32 levels, TextureType type, vk::Format format, - vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, - vk::ImageAspectFlags aspect, bool need_format_list, + vk::SampleCountFlagBits samples, vk::ImageUsageFlags usage, + vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, std::string_view debug_name) { Destroy(); @@ -197,7 +202,7 @@ void Handle::Create(u32 width, u32 height, u32 levels, TextureType type, vk::For .extent = {width, height, 1}, .mipLevels = levels, .arrayLayers = layers, - .samples = vk::SampleCountFlagBits::e1, + .samples = samples, .usage = usage, }; @@ -354,7 +359,10 @@ bool TextureRuntime::ClearTexture(Surface& surface, const VideoCore::TextureClea .src_image = surface.Image(), }; - if (clear.texture_rect == surface.GetScaledRect()) { + // MSAA images should always use a render-pass to clear both the MSAA texture and the regular + // texture at the same time + + if (clear.texture_rect == surface.GetScaledRect() && (surface.GetSampleCount() == 1)) { scheduler.Record([params, clear](vk::CommandBuffer cmdbuf) { const vk::ImageSubresourceRange range = { .aspectMask = params.aspect, @@ -417,7 +425,8 @@ void TextureRuntime::ClearTextureWithRenderpass(Surface& surface, const auto color_format = is_color ? surface.pixel_format : PixelFormat::Invalid; const auto depth_format = is_color ? PixelFormat::Invalid : surface.pixel_format; - const auto render_pass = renderpass_cache.GetRenderpass(color_format, depth_format, true); + const auto render_pass = + renderpass_cache.GetRenderpass(color_format, depth_format, true, surface.GetSampleCount()); const RecordParams params = { .aspect = surface.Aspect(), @@ -426,8 +435,11 @@ void TextureRuntime::ClearTextureWithRenderpass(Surface& surface, .src_image = surface.Image(), }; - scheduler.Record([params, is_color, clear, render_pass, - framebuffer = surface.Framebuffer()](vk::CommandBuffer cmdbuf) { + // Ensure we get the MSAA framebuffer if we are are doing an MSAA texture + const vk::Framebuffer framebuffer = + surface.Framebuffer((surface.GetSampleCount() > 1) ? Type::MultiSampled : Type::Current); + + scheduler.Record([params, is_color, clear, render_pass, framebuffer](vk::CommandBuffer cmdbuf) { const vk::AccessFlags access_flag = is_color ? vk::AccessFlagBits::eColorAttachmentRead | vk::AccessFlagBits::eColorAttachmentWrite @@ -473,13 +485,14 @@ void TextureRuntime::ClearTextureWithRenderpass(Surface& surface, }; const auto clear_value = MakeClearValue(clear.value); + std::array clear_values = {clear_value, clear_value}; const vk::RenderPassBeginInfo renderpass_begin_info = { .renderPass = render_pass, .framebuffer = framebuffer, .renderArea = render_area, - .clearValueCount = 1, - .pClearValues = &clear_value, + .clearValueCount = static_cast(clear_values.size()), + .pClearValues = clear_values.data(), }; cmdbuf.pipelineBarrier(params.pipeline_flags, pipeline_flags, @@ -605,6 +618,15 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, renderpass_cache.EndRendering(); + // Must resolve images first + // Todo(wunk): Add a "dirty" flag for msaa resolves to avoid redundant image resolves + if (source.sample_count > 1) { + blit_helper.ResolveTexture(source); + } + if (dest.sample_count > 1) { + blit_helper.ResolveTexture(dest); + } + const RecordParams params = { .aspect = source.Aspect(), .filter = vk::Filter::eNearest, @@ -742,8 +764,8 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param const VideoCore::SurfaceFlagBits& initial_flag_bits) : SurfaceBase{params, initial_flag_bits}, runtime{runtime_}, instance{runtime_.GetInstance()}, scheduler{runtime_.GetScheduler()}, traits{instance.GetTraits(pixel_format)}, - handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance)} { - + handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance), + Handle(instance)} { if (pixel_format == VideoCore::PixelFormat::Invalid || !traits.transfer_support) { return; } @@ -764,8 +786,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param ASSERT_MSG(format != vk::Format::eUndefined && levels >= 1, "Image allocation parameters are invalid"); - u32 num_images{}; - std::array raw_images; + boost::container::static_vector raw_images; vk::ImageCreateFlags flags{}; if (texture_type == VideoCore::TextureType::CubeMap) { @@ -789,26 +810,38 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param } const bool need_format_list = is_mutable && instance.IsImageFormatListSupported(); - handles[Type::Base].Create(width, height, levels, texture_type, format, usage, flags, - traits.aspect, need_format_list, DebugName(false)); - raw_images[num_images++] = handles[Type::Base].image; + handles[Type::Base].Create(width, height, levels, texture_type, format, + vk::SampleCountFlagBits::e1, usage, flags, traits.aspect, + need_format_list, DebugName(false)); + raw_images.emplace_back(handles[Type::Base].image); + // Upscaled image if (res_scale != 1) { handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, - format, usage, flags, traits.aspect, need_format_list, - DebugName(true)); - raw_images[num_images++] = handles[Type::Scaled].image; + format, vk::SampleCountFlagBits::e1, usage, flags, + traits.aspect, need_format_list, DebugName(true)); + raw_images.emplace_back(handles[Type::Scaled].image); + } + + // Upscaled+MSAA image + if (sample_count > 1) { + handles[Type::MultiSampled].Create( + GetScaledWidth(), GetScaledHeight(), levels, texture_type, format, + vk::SampleCountFlagBits(sample_count), traits.usage, flags, traits.aspect, + need_format_list, DebugName(true, false, sample_count)); + raw_images.emplace_back(handles[Type::MultiSampled].image); } current = res_scale != 1 ? Type::Scaled : Type::Base; runtime.renderpass_cache.EndRendering(); - scheduler.Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { - std::array barriers; - MakeInitBarriers(aspect, num_images, raw_images, barriers); - cmdbuf.pipelineBarrier( - vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, - vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, num_images, barriers.data()); + scheduler.Record([raw_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + std::array barriers; + MakeInitBarriers(aspect, raw_images, barriers); + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, + raw_images.size(), barriers.data()); }); } @@ -816,7 +849,8 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface const VideoCore::Material* mat) : SurfaceBase{surface}, runtime{runtime_}, instance{runtime_.GetInstance()}, scheduler{runtime_.GetScheduler()}, traits{instance.GetTraits(mat->format)}, - handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance)} { + handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance), + Handle(instance)} { if (!traits.transfer_support) { return; } @@ -824,8 +858,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface const bool has_normal = mat && mat->Map(MapType::Normal); const vk::Format format = traits.native; - u32 num_images{}; - std::array raw_images; + boost::container::static_vector raw_images; vk::ImageCreateFlags flags{}; if (texture_type == VideoCore::TextureType::CubeMap) { @@ -833,31 +866,41 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface } const std::string debug_name = DebugName(false, true); - handles[Type::Base].Create(mat->width, mat->height, levels, texture_type, format, traits.usage, - flags, traits.aspect, false, debug_name); - raw_images[num_images++] = handles[Type::Base].image; + handles[Type::Base].Create(mat->width, mat->height, levels, texture_type, format, + vk::SampleCountFlagBits::e1, traits.usage, flags, traits.aspect, + false, debug_name); + raw_images.emplace_back(handles[Type::Base].image); if (res_scale != 1) { handles[Type::Scaled].Create(mat->width, mat->height, levels, texture_type, - vk::Format::eR8G8B8A8Unorm, traits.usage, flags, traits.aspect, - false, debug_name); - raw_images[num_images++] = handles[Type::Scaled].image; + vk::Format::eR8G8B8A8Unorm, vk::SampleCountFlagBits::e1, + traits.usage, flags, traits.aspect, false, debug_name); + raw_images.emplace_back(handles[Type::Scaled].image); + } + if (sample_count > 1) { + handles[Type::MultiSampled].Create( + GetScaledWidth(), GetScaledHeight(), levels, texture_type, format, + vk::SampleCountFlagBits(sample_count), traits.usage, flags, traits.aspect, false, + DebugName(res_scale != 1, true, sample_count)); + raw_images.emplace_back(handles[Type::MultiSampled].image); } if (has_normal) { handles[Type::Custom].Create(mat->width, mat->height, levels, texture_type, format, - traits.usage, flags, traits.aspect, false, debug_name); - raw_images[num_images++] = handles[Type::Custom].image; + vk::SampleCountFlagBits::e1, traits.usage, flags, + traits.aspect, false, debug_name); + raw_images.emplace_back(handles[Type::Custom].image); } current = res_scale != 1 ? Type::Scaled : Type::Base; runtime.renderpass_cache.EndRendering(); - scheduler.Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { - std::array barriers; - MakeInitBarriers(aspect, num_images, raw_images, barriers); - cmdbuf.pipelineBarrier( - vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, - vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, num_images, barriers.data()); + scheduler.Record([raw_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + std::array barriers; + MakeInitBarriers(aspect, raw_images, barriers); + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, + raw_images.size(), barriers.data()); }); custom_format = mat->format; @@ -1112,13 +1155,7 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, }); } -void Surface::ScaleUp(u32 new_scale) { - if (res_scale == new_scale || new_scale == 1) { - return; - } - - res_scale = new_scale; - +void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { const bool is_mutable = pixel_format == VideoCore::PixelFormat::RGBA8; vk::ImageCreateFlags flags{}; @@ -1129,31 +1166,59 @@ void Surface::ScaleUp(u32 new_scale) { flags |= vk::ImageCreateFlagBits::eMutableFormat; } - handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, - traits.native, traits.usage, flags, traits.aspect, false, - DebugName(true)); - current = Type::Scaled; + const bool res_scale_modified = res_scale != new_scale; + if (res_scale_modified && new_scale > 1) { + res_scale = new_scale; - handles[Type::Copy].Destroy(); + handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, + traits.native, vk::SampleCountFlagBits::e1, traits.usage, + flags, traits.aspect, false, DebugName(true)); + current = Type::Scaled; - runtime.renderpass_cache.EndRendering(); - scheduler.Record( - [raw_images = std::array{Image()}, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + handles[Type::Copy].Destroy(); + + runtime.renderpass_cache.EndRendering(); + scheduler.Record( + [raw_images = std::array{Image()}, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + std::array barriers; + MakeInitBarriers(aspect, 1, raw_images, barriers); + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, {}, {}, barriers); + }); + + for (u32 level = 0; level < levels; level++) { + const VideoCore::TextureBlit blit = { + .src_level = level, + .dst_level = level, + .src_rect = GetRect(level), + .dst_rect = GetScaledRect(level), + }; + BlitScale(blit, true); + } + } + + if ((res_scale_modified || sample_count != new_sample_count) && new_sample_count > 1) { + sample_count = new_sample_count; + handles[Type::MultiSampled].Create( + GetScaledWidth(), GetScaledHeight(), levels, texture_type, traits.native, + vk::SampleCountFlagBits(sample_count), traits.usage, flags, traits.aspect, false, + DebugName(res_scale != 1, false, sample_count)); + + // The multi-sampled image is just a transient image that is almost always immediately + // resolved into the current image, and should not be representative of the entire surface! + // + // current = Type::MultiSampled; + + runtime.renderpass_cache.EndRendering(); + scheduler.Record([raw_images = std::array{Image(Type::MultiSampled)}, + aspect = traits.aspect](vk::CommandBuffer cmdbuf) { std::array barriers; MakeInitBarriers(aspect, 1, raw_images, barriers); cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, vk::DependencyFlagBits::eByRegion, {}, {}, barriers); }); - - for (u32 level = 0; level < levels; level++) { - const VideoCore::TextureBlit blit = { - .src_level = level, - .dst_level = level, - .src_rect = GetRect(level), - .dst_rect = GetScaledRect(level), - }; - BlitScale(blit, true); } } @@ -1203,7 +1268,7 @@ vk::ImageView Surface::CopyImageView() noexcept { flags |= vk::ImageCreateFlagBits::eCubeCompatible; } copy_handle.Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, traits.native, - traits.usage, flags, traits.aspect, false); + vk::SampleCountFlagBits::e1, traits.usage, flags, traits.aspect, false); copy_layout = vk::ImageLayout::eUndefined; } @@ -1337,7 +1402,8 @@ vk::ImageView Surface::ImageView(ViewType view_type, Type type) noexcept { } vk::Framebuffer Surface::Framebuffer(Type type) noexcept { - auto& handle = handles[type == Type::Current ? current : type]; + type = (type == Type::Current) ? current : type; + auto& handle = handles[type]; if (handle.framebuffer) { return handle.framebuffer; } @@ -1347,11 +1413,20 @@ vk::Framebuffer Surface::Framebuffer(Type type) noexcept { const auto color_format = is_depth ? PixelFormat::Invalid : pixel_format; const auto depth_format = is_depth ? pixel_format : PixelFormat::Invalid; - const auto image_view = ImageView(ViewType::Mip0, type); + boost::container::small_vector image_views; + if (sample_count > 1) { + // Main surface + MSAA surface + image_views.emplace_back(ImageView(ViewType::Mip0, current)); + image_views.emplace_back(ImageView(ViewType::Mip0, Type::MultiSampled)); + } else { + image_views.emplace_back(ImageView(ViewType::Mip0, type)); + } + const vk::FramebufferCreateInfo framebuffer_info = { - .renderPass = runtime.renderpass_cache.GetRenderpass(color_format, depth_format, false), - .attachmentCount = 1u, - .pAttachments = &image_view, + .renderPass = + runtime.renderpass_cache.GetRenderpass(color_format, depth_format, false, sample_count), + .attachmentCount = static_cast(image_views.size()), + .pAttachments = image_views.data(), .width = handle.width, .height = handle.height, .layers = handle.layers, @@ -1467,7 +1542,8 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferParams& params, Surface* color, Surface* depth) : VideoCore::FramebufferParams{params}, instance{runtime.GetInstance()}, - res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)} { + res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)}, + sample_count{color ? color->sample_count : (depth ? depth->sample_count : u8(1u))} { auto& renderpass_cache = runtime.GetRenderpassCache(); if (shadow_rendering && !color) { return; @@ -1476,7 +1552,7 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa width = height = std::numeric_limits::max(); u32 num_attachments{}; - std::array attachments; + std::array attachments; const auto prepare = [&](u32 index, Surface* surface) { const auto extent = surface->RealExtent(); @@ -1492,8 +1568,8 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa const auto extent = color->RealExtent(); width = extent.width; height = extent.height; - render_pass = - renderpass_cache.GetRenderpass(PixelFormat::Invalid, PixelFormat::Invalid, false); + render_pass = renderpass_cache.GetRenderpass(PixelFormat::Invalid, PixelFormat::Invalid, + false, sample_count); images[0] = color->Image(); image_views[0] = color->StorageView(); aspects[0] = vk::ImageAspectFlagBits::eColor; @@ -1506,7 +1582,23 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa prepare(1, depth); attachments[num_attachments++] = image_views[1]; } - render_pass = renderpass_cache.GetRenderpass(formats[0], formats[1], false); + + // MSAA attachments + if (sample_count > 1) { + if (color) { + images[2] = color->Image(Type::MultiSampled); + image_views[2] = color->ImageView(ViewType::Mip0, Type::MultiSampled); + aspects[2] = color->Aspect(); + attachments[num_attachments++] = image_views[2]; + } + if (depth) { + images[3] = depth->Image(Type::MultiSampled); + image_views[3] = depth->ImageView(ViewType::Mip0, Type::MultiSampled); + aspects[3] = depth->Aspect(); + attachments[num_attachments++] = image_views[3]; + } + } + render_pass = renderpass_cache.GetRenderpass(formats[0], formats[1], false, sample_count); } const vk::FramebufferCreateInfo framebuffer_info = { diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index 8a35189b6..b10fd4d73 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -29,6 +29,7 @@ enum Type { Current = -1, Base = 0, Scaled, + MultiSampled, Custom, Copy, Num, @@ -79,8 +80,9 @@ struct Handle { } void Create(u32 width, u32 height, u32 levels, VideoCore::TextureType type, vk::Format format, - vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, - bool need_format_list, std::string_view debug_name = {}); + vk::SampleCountFlagBits samples, vk::ImageUsageFlags usage, + vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, + std::string_view debug_name = {}); void Destroy(); @@ -260,8 +262,8 @@ public: void Download(const VideoCore::BufferTextureCopy& download, const VideoCore::StagingData& staging); - /// Scales up the surface to match the new resolution scale. - void ScaleUp(u32 new_scale); + /// Scales up the surface to match the new resolution scale and sample-count. + void ScaleUp(u32 new_scale, u8 new_sample_count); /// Returns the bpp of the internal surface format u32 GetInternalBytesPerPixel() const; @@ -314,7 +316,8 @@ public: formats(std::exchange( other.formats, {VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid})), width(std::exchange(other.width, 0)), height(std::exchange(other.height, 0)), - res_scale(std::exchange(other.res_scale, 1)) {} + res_scale(std::exchange(other.res_scale, 1)), + sample_count(std::exchange(other.sample_count, 1)) {} Framebuffer& operator=(Framebuffer&& other) noexcept { VideoCore::FramebufferParams::operator=(std::move(other)); @@ -329,6 +332,7 @@ public: width = std::exchange(other.width, 0); height = std::exchange(other.height, 0); res_scale = std::exchange(other.res_scale, 1); + sample_count = std::exchange(other.sample_count, 1); return *this; } @@ -345,11 +349,11 @@ public: return framebuffer; } - [[nodiscard]] std::array Images() const noexcept { + [[nodiscard]] std::array Images() const noexcept { return images; } - [[nodiscard]] std::array Aspects() const noexcept { + [[nodiscard]] std::array Aspects() const noexcept { return aspects; } @@ -361,19 +365,26 @@ public: return res_scale; } + u8 Samples() const noexcept { + return sample_count; + } + private: const Instance& instance; - std::array images{}; - std::array image_views{}; + // Color, Depth, ColorMSAA, DepthMSAA + std::array images{}; + std::array image_views{}; vk::Framebuffer framebuffer{}; vk::RenderPass render_pass{}; std::vector framebuffer_views; - std::array aspects{}; - std::array formats{VideoCore::PixelFormat::Invalid, - VideoCore::PixelFormat::Invalid}; + std::array aspects{}; + std::array formats{ + VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid, + VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid}; u32 width{}; u32 height{}; u32 res_scale{1}; + u8 sample_count{1}; }; class Sampler {