From 14736c70651c66f466db3257b2a877e261a034ae Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 18 Apr 2026 20:41:43 -0700 Subject: [PATCH 01/29] config: Add `sample_count` renderer option Option is only enabled when the renderer is set to Vulkan, for now. --- src/android/app/src/main/jni/config.cpp | 1 + src/citra_qt/configuration/config.cpp | 2 + .../configuration/configure_enhancements.cpp | 13 ++++ .../configuration/configure_enhancements.ui | 63 ++++++++++++++++--- src/common/settings.cpp | 2 + src/common/settings.h | 1 + src/video_core/renderer_base.cpp | 11 ++++ src/video_core/renderer_base.h | 3 + 8 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 7a2746224..7411f1e62 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -150,6 +150,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.sample_count); ReadSetting("Renderer", Settings::values.use_disk_shader_cache); ReadSetting("Renderer", Settings::values.use_vsync); ReadSetting("Renderer", Settings::values.texture_filter); diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 9091d734b..b545f0424 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -716,6 +716,7 @@ void QtConfig::ReadRendererValues() { ReadGlobalSetting(Settings::values.use_vsync); ReadGlobalSetting(Settings::values.use_display_refresh_rate_detection); ReadGlobalSetting(Settings::values.resolution_factor); + ReadGlobalSetting(Settings::values.sample_count); ReadGlobalSetting(Settings::values.use_integer_scaling); ReadGlobalSetting(Settings::values.frame_limit); ReadGlobalSetting(Settings::values.turbo_limit); @@ -1265,6 +1266,7 @@ void QtConfig::SaveRendererValues() { WriteGlobalSetting(Settings::values.use_vsync); WriteGlobalSetting(Settings::values.use_display_refresh_rate_detection); WriteGlobalSetting(Settings::values.resolution_factor); + WriteGlobalSetting(Settings::values.sample_count); 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 62afcbb77..01bd0cb8a 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -21,6 +21,8 @@ ConfigureEnhancements::ConfigureEnhancements(QWidget* parent) const auto graphics_api = Settings::values.graphics_api.GetValue(); const bool res_scale_enabled = graphics_api != Settings::GraphicsAPI::Software; ui->resolution_factor_combobox->setEnabled(res_scale_enabled); + const bool msaa_enabled = graphics_api == Settings::GraphicsAPI::Vulkan; + ui->sample_count_combobox->setEnabled(msaa_enabled); connect(ui->render_3d_combobox, qOverload(&QComboBox::currentIndexChanged), this, [this](int currentIndex) { @@ -44,6 +46,8 @@ void ConfigureEnhancements::SetConfiguration() { if (!Settings::IsConfiguringGlobal()) { ConfigurationShared::SetPerGameSetting(ui->resolution_factor_combobox, &Settings::values.resolution_factor); + ConfigurationShared::SetPerGameSetting(ui->sample_count_combobox, + &Settings::values.sample_count); ConfigurationShared::SetPerGameSetting(ui->texture_filter_combobox, &Settings::values.texture_filter); ConfigurationShared::SetHighlight(ui->widget_texture_filter, @@ -51,6 +55,8 @@ void ConfigureEnhancements::SetConfiguration() { } else { ui->resolution_factor_combobox->setCurrentIndex( Settings::values.resolution_factor.GetValue()); + ui->sample_count_combobox->setCurrentIndex( + static_cast(Settings::values.sample_count.GetValue())); ui->texture_filter_combobox->setCurrentIndex( static_cast(Settings::values.texture_filter.GetValue())); } @@ -111,6 +117,8 @@ void ConfigureEnhancements::RetranslateUI() { void ConfigureEnhancements::ApplyConfiguration() { ConfigurationShared::ApplyPerGameSetting(&Settings::values.resolution_factor, ui->resolution_factor_combobox); + ConfigurationShared::ApplyPerGameSetting(&Settings::values.sample_count, + ui->sample_count_combobox); Settings::values.render_3d = static_cast(ui->render_3d_combobox->currentIndex()); Settings::values.swap_eyes_3d = ui->swap_eyes_3d->isChecked(); @@ -149,6 +157,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_sample_count->setEnabled(Settings::values.sample_count.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 +198,10 @@ void ConfigureEnhancements::SetupPerGameUI() { ui->resolution_factor_combobox, ui->widget_resolution, static_cast(Settings::values.resolution_factor.GetValue(true))); + ConfigurationShared::SetColoredComboBox( + ui->sample_count_combobox, ui->widget_sample_count, + static_cast(Settings::values.sample_count.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 dc3c3deaf..f040fb7ce 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -111,15 +111,64 @@ - - - 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 + + + + + Sample count + + + + + + + + Native (x1) + + + + + x2 + + + + + x4 + + + + + x8 + + + + + + + + + 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 df8451e28..56a6200c9 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -97,6 +97,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.sample_count.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()); @@ -213,6 +214,7 @@ void RestoreGlobalState(bool is_powered_on) { values.shaders_accurate_mul.SetGlobal(true); values.use_vsync.SetGlobal(true); values.resolution_factor.SetGlobal(true); + values.sample_count.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 4196557d1..b0d0ccb9e 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -535,6 +535,7 @@ struct Values { true, Keys::use_display_refresh_rate_detection}; Setting use_shader_jit{true, Keys::use_shader_jit}; SwitchableSetting resolution_factor{1, 0, 10, Keys::resolution_factor}; + SwitchableSetting sample_count{0, 0, 3, "sample_count"}; 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/renderer_base.cpp b/src/video_core/renderer_base.cpp index e78ab9c91..8331664da 100644 --- a/src/video_core/renderer_base.cpp +++ b/src/video_core/renderer_base.cpp @@ -29,6 +29,17 @@ u32 RendererBase::GetResolutionScaleFactor() { : render_window.GetFramebufferLayout().GetScalingRatio(); } +u8 RendererBase::GetSampleCount() const { + const auto graphics_api = Settings::values.graphics_api.GetValue(); + + // Enabled for vulkan only for now + if (graphics_api != Settings::GraphicsAPI::Vulkan) { + return 1; + } + + return static_cast(1u << Settings::values.sample_count.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 5a737a37f..fa647faa4 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 = {}); From a0eea7bb4d79125b0e62315425702f5f88217a50 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 18 Apr 2026 21:08:24 -0700 Subject: [PATCH 02/29] vk_blit_helper: Add `d24s8_to_rgba8_ms_comp` Helper host-shader for blitting multi-sampled DS24S8 textures to multi-sampled RGBA8 --- src/video_core/host_shaders/CMakeLists.txt | 3 +- .../vulkan_d24s8_to_rgba8_ms.comp | 31 +++++++++++++++++++ .../renderer_vulkan/vk_blit_helper.cpp | 24 +++++++++++--- .../renderer_vulkan/vk_blit_helper.h | 2 ++ 4 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 src/video_core/host_shaders/format_reinterpreter/vulkan_d24s8_to_rgba8_ms.comp diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 964000d92..8018a9c55 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -1,4 +1,4 @@ -# 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. @@ -6,6 +6,7 @@ set(SHADER_FILES format_reinterpreter/d24s8_to_rgba8.frag format_reinterpreter/rgba4_to_rgb5a1.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/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..3dca7c91e --- /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() { + int sample_count = textureSamples(depth); + + ivec2 src_coord = src_offset + ivec2(gl_GlobalInvocationID.xy); + ivec2 dst_coord = dst_offset + ivec2(gl_GlobalInvocationID.xy); + 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/renderer_vulkan/vk_blit_helper.cpp b/src/video_core/renderer_vulkan/vk_blit_helper.cpp index 9982948b3..7903d671d 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_helper.cpp @@ -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,8 @@ 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)}, + d24s8_to_rgba8_ms_comp{Compile(HostShaders::VULKAN_D24S8_TO_RGBA8_MS_COMP, + vk::ShaderStageFlagBits::eCompute, device)}, depth_to_buffer_comp{Compile(HostShaders::VULKAN_DEPTH_TO_BUFFER_COMP, vk::ShaderStageFlagBits::eCompute, device)}, blit_depth_stencil_frag{VK_NULL_HANDLE}, @@ -260,6 +263,8 @@ 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)}, + d24s8_to_rgba8_ms_pipeline{ + MakeComputePipeline(d24s8_to_rgba8_ms_comp, compute_pipeline_layout)}, depth_to_buffer_pipeline{ MakeComputePipeline(depth_to_buffer_comp, compute_buffer_pipeline_layout)}, depth_blit_pipeline{VK_NULL_HANDLE}, @@ -289,6 +294,7 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, SetObjectName(device, blit_depth_stencil_frag, "BlitHelper: blit_depth_stencil_frag"); } SetObjectName(device, d24s8_to_rgba8_pipeline, "BlitHelper: d24s8_to_rgba8_pipeline"); + SetObjectName(device, d24s8_to_rgba8_ms_pipeline, "BlitHelper: d24s8_to_rgba8_ms_pipeline"); SetObjectName(device, depth_to_buffer_pipeline, "BlitHelper: depth_to_buffer_pipeline"); if (depth_blit_pipeline) { SetObjectName(device, depth_blit_pipeline, "BlitHelper: depth_blit_pipeline"); @@ -322,6 +328,7 @@ BlitHelper::~BlitHelper() { device.destroyShaderModule(refine_frag); device.destroyPipeline(depth_to_buffer_pipeline); device.destroyPipeline(d24s8_to_rgba8_pipeline); + device.destroyPipeline(d24s8_to_rgba8_ms_pipeline); device.destroyPipeline(depth_blit_pipeline); device.destroySampler(linear_sampler); device.destroySampler(nearest_sampler); @@ -401,15 +408,22 @@ 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; + const auto pipeline = multisample ? d24s8_to_rgba8_ms_pipeline : d24s8_to_rgba8_pipeline; + const auto descriptor_set = compute_provider.Commit(); - update_queue.AddImageSampler(descriptor_set, 0, 0, source.DepthView(), VK_NULL_HANDLE, - vk::ImageLayout::eDepthStencilReadOnlyOptimal); - update_queue.AddImageSampler(descriptor_set, 1, 0, source.StencilView(), 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.AddStorageImage(descriptor_set, 2, dest.ImageView()); renderpass_cache.EndRendering(); - scheduler.Record([this, descriptor_set, copy, src_image = source.Image(), + + scheduler.Record([this, pipeline, descriptor_set, copy, src_image = source.Image(), dst_image = dest.Image()](vk::CommandBuffer cmdbuf) { const std::array pre_barriers = { vk::ImageMemoryBarrier{ @@ -488,7 +502,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), diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.h b/src/video_core/renderer_vulkan/vk_blit_helper.h index 59aee655f..16e0090b0 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.h +++ b/src/video_core/renderer_vulkan/vk_blit_helper.h @@ -83,6 +83,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 +93,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; From 29eb887d9032e8d98f2aca2ccc486c958386b149 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 18 Apr 2026 21:37:26 -0700 Subject: [PATCH 03/29] 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. --- src/video_core/renderer_vulkan/vk_instance.cpp | 4 ++++ src/video_core/renderer_vulkan/vk_instance.h | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index d86cc0f67..e3c90aff4 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -449,6 +449,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); @@ -518,9 +520,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 f6fbc28fb..e7420278c 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -251,6 +251,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; @@ -330,6 +335,8 @@ protected: bool index_type_uint8{}; 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{}; From ddc81c390cbafb4572d1a4ff60097f5be7d04cb1 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 18 Apr 2026 22:33:24 -0700 Subject: [PATCH 04/29] rasterizer_cache: Initial support for multi-sample surfaces --- .../rasterizer_cache/framebuffer_base.h | 3 +- .../rasterizer_cache/rasterizer_cache.h | 43 ++++++++++--- .../rasterizer_cache/rasterizer_cache_base.h | 1 + .../rasterizer_cache/surface_params.cpp | 9 +-- .../rasterizer_cache/surface_params.h | 7 ++- .../renderer_opengl/gl_texture_runtime.cpp | 32 ++++++---- .../renderer_opengl/gl_texture_runtime.h | 4 +- .../renderer_vulkan/vk_texture_runtime.cpp | 60 +++++++++++-------- .../renderer_vulkan/vk_texture_runtime.h | 5 +- 9 files changed, 109 insertions(+), 55 deletions(-) diff --git a/src/video_core/rasterizer_cache/framebuffer_base.h b/src/video_core/rasterizer_cache/framebuffer_base.h index 2f2545849..f7387b79f 100644 --- a/src/video_core/rasterizer_cache/framebuffer_base.h +++ b/src/video_core/rasterizer_cache/framebuffer_base.h @@ -27,7 +27,8 @@ struct FramebufferParams { u32 color_level; u32 depth_level; bool shadow_rendering; - INSERT_PADDING_BYTES(3); + u8 sample_count; + INSERT_PADDING_BYTES(2); bool operator==(const FramebufferParams& params) const noexcept { return std::memcmp(this, ¶ms, sizeof(FramebufferParams)) == 0; diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index 26156cb56..fbf53205a 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; @@ -96,12 +96,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(); @@ -287,6 +290,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. @@ -302,6 +306,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); @@ -432,8 +437,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); @@ -502,6 +509,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); @@ -706,6 +714,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; @@ -771,6 +780,7 @@ FramebufferHelper RasterizerCache::GetFramebufferSurfaces(bool using_color .color_level = color_level, .depth_level = depth_level, .shadow_rendering = regs.framebuffer.IsShadowRendering(), + .sample_count = sample_count, }; auto [it, new_framebuffer] = framebuffers.try_emplace(fb_params); @@ -861,12 +871,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 @@ -886,11 +900,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; }; @@ -901,6 +920,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; @@ -1189,8 +1215,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); @@ -1357,8 +1384,8 @@ SurfaceId RasterizerCache::CreateSurface(const SurfaceParams& params, return surface_id; }(); Surface& surface = slot_surfaces[surface_id]; - if (params.res_scale > surface.res_scale) { - surface.ScaleUp(params.res_scale); + if ((params.res_scale > surface.res_scale) || (params.sample_count > surface.sample_count)) { + 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 406c374dc..0ddf6a713 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache_base.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache_base.h @@ -227,6 +227,7 @@ private: SurfaceMap dirty_regions; PageMap cached_pages; u32 resolution_scale_factor; + u8 sample_count; u64 frame_tick{}; FramebufferParams fb_params; Settings::TextureFilter filter; diff --git a/src/video_core/rasterizer_cache/surface_params.cpp b/src/video_core/rasterizer_cache/surface_params.cpp index 74ae8205c..fe1db9ca2 100644 --- a/src/video_core/rasterizer_cache/surface_params.cpp +++ b/src/video_core/rasterizer_cache/surface_params.cpp @@ -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 23e4db7e2..9ae8aadb0 100644 --- a/src/video_core/rasterizer_cache/surface_params.h +++ b/src/video_core/rasterizer_cache/surface_params.h @@ -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_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index fc14e949f..4ce41a0fc 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -556,23 +556,31 @@ void Surface::Attach(GLenum target, u32 level, u32 layer, bool scaled) { } } -void Surface::ScaleUp(u32 new_scale) { - if (res_scale == new_scale || new_scale == 1) { +void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { + if (res_scale == new_scale && sample_count == new_sample_count) { return; } res_scale = new_scale; - textures[1] = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, tuple, - DebugName(true)); + sample_count = new_sample_count; - 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 > 1) { + + 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 (new_sample_count > 1) { + // Todo(wunk): OpenGL MSAA } } diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.h b/src/video_core/renderer_opengl/gl_texture_runtime.h index d25ba102c..434a3c464 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.h +++ b/src/video_core/renderer_opengl/gl_texture_runtime.h @@ -132,8 +132,8 @@ public: /// Attaches a handle of surface to the specified framebuffer target void Attach(GLenum target, u32 level, u32 layer, bool scaled = true); - /// 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; diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index c62757384..51adebb87 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -731,7 +731,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; @@ -800,7 +801,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; } @@ -1096,12 +1098,13 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, }); } -void Surface::ScaleUp(u32 new_scale) { - if (res_scale == new_scale || new_scale == 1) { +void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { + if (res_scale == new_scale && sample_count == new_sample_count) { return; } res_scale = new_scale; + sample_count = new_sample_count; const bool is_mutable = pixel_format == VideoCore::PixelFormat::RGBA8; @@ -1113,29 +1116,36 @@ 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; + if (res_scale > 1) { - 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); - }); + handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, + traits.native, traits.usage, flags, traits.aspect, false, + DebugName(true)); + current = Type::Scaled; - 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); + 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 (new_sample_count > 1) { + // Todo(wunk): Vulkan MSAA } } diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index b46479c58..98b140f96 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, @@ -252,8 +253,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; From 950803c2b7cc3e5c5bf02465749a37d7c5f5fc82 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 19 Apr 2026 09:40:23 -0700 Subject: [PATCH 05/29] 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/vk_graphics_pipeline.cpp | 8 +- .../renderer_vulkan/vk_graphics_pipeline.h | 3 +- .../renderer_vulkan/vk_render_manager.cpp | 175 ++++++++++++++++-- .../renderer_vulkan/vk_render_manager.h | 21 ++- .../renderer_vulkan/vk_shader_disk_cache.h | 4 +- 5 files changed, 182 insertions(+), 29 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 7c078802d..586a7f500 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 vk::PipelineColorBlendAttachmentState colorblend_attachment = { @@ -275,7 +276,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 5f817094e..8ab0933c2 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -214,6 +214,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; @@ -225,7 +226,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_render_manager.cpp b/src/video_core/renderer_vulkan/vk_render_manager.cpp index b3615e1ae..73885f497 100644 --- a/src/video_core/renderer_vulkan/vk_render_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_render_manager.cpp @@ -2,6 +2,8 @@ // Licensed under GPLv2 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 3ebbd817b..0b5fdf606 100644 --- a/src/video_core/renderer_vulkan/vk_render_manager.h +++ b/src/video_core/renderer_vulkan/vk_render_manager.h @@ -4,6 +4,7 @@ #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 = 8; + 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 e05d43d09..51152cd59 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h @@ -142,13 +142,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 { From fdfe63f76c81519f705c3eeb348529bb847eb9d8 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 19 Apr 2026 09:42:38 -0700 Subject: [PATCH 06/29] renderer_vulkan: Implement multisample texture runtime This seems to be enough for simple programs to render with MSAA enabled! --- .../rasterizer_cache/surface_params.cpp | 2 +- .../rasterizer_cache/surface_params.h | 2 +- .../renderer_vulkan/vk_rasterizer.cpp | 2 + .../renderer_vulkan/vk_texture_runtime.cpp | 277 ++++++++++++++---- .../renderer_vulkan/vk_texture_runtime.h | 26 +- 5 files changed, 239 insertions(+), 70 deletions(-) diff --git a/src/video_core/rasterizer_cache/surface_params.cpp b/src/video_core/rasterizer_cache/surface_params.cpp index fe1db9ca2..4e4e3cc69 100644 --- a/src/video_core/rasterizer_cache/surface_params.cpp +++ b/src/video_core/rasterizer_cache/surface_params.cpp @@ -1,4 +1,4 @@ -// Copyright 2022 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/video_core/rasterizer_cache/surface_params.h b/src/video_core/rasterizer_cache/surface_params.h index 9ae8aadb0..fe78e974e 100644 --- a/src/video_core/rasterizer_cache/surface_params.h +++ b/src/video_core/rasterizer_cache/surface_params.h @@ -1,4 +1,4 @@ -// Copyright 2022 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 0f997d5e9..0fb9ab449 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -558,6 +558,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(); @@ -777,6 +778,7 @@ bool RasterizerVulkan::AccelerateDisplay(const Pica::FramebufferConfig& config, src_params.stride = pixel_stride; src_params.is_tiled = false; src_params.pixel_format = VideoCore::PixelFormatFromGPUPixelFormat(config.color_format); + src_params.sample_count = (1u << Settings::values.sample_count.GetValue()); src_params.UpdateParams(); const auto [src_surface_id, src_rect] = diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 51adebb87..875658815 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) { const bool is_cube_map = type == TextureType::CubeMap && instance.IsLayeredRenderingSupported(); if (!is_cube_map) { @@ -194,7 +199,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, }; @@ -344,7 +349,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, @@ -407,7 +415,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(), @@ -462,13 +471,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, @@ -604,6 +614,94 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, .dst_image = dest.Image(), }; + // Todo(wunk): Add a "dirty" flag for msaa resolves to avoid redundant image resolves + const auto resolve_image = [&](Surface& msaa_surface) { + scheduler.Record([&msaa_surface](vk::CommandBuffer cmdbuf) { + const vk::ImageResolve resolve_area = { + .srcSubresource{ + .aspectMask = msaa_surface.Aspect(), + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .srcOffset = {}, + .dstSubresource{ + .aspectMask = msaa_surface.Aspect(), + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .dstOffset = {}, + .extent{msaa_surface.GetScaledWidth(), msaa_surface.GetScaledHeight(), 1}, + }; + + const std::array read_barriers = { + vk::ImageMemoryBarrier{ + .srcAccessMask = msaa_surface.AccessFlags(), + .dstAccessMask = vk::AccessFlagBits::eTransferRead, + .oldLayout = vk::ImageLayout::eGeneral, + .newLayout = vk::ImageLayout::eTransferSrcOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = msaa_surface.Image(Type::MultiSampled), + .subresourceRange = MakeSubresourceRange(msaa_surface.Aspect(), 0), + }, + vk::ImageMemoryBarrier{ + .srcAccessMask = msaa_surface.AccessFlags(), + .dstAccessMask = vk::AccessFlagBits::eTransferWrite, + .oldLayout = vk::ImageLayout::eGeneral, + .newLayout = vk::ImageLayout::eTransferDstOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = msaa_surface.Image(), + .subresourceRange = MakeSubresourceRange(msaa_surface.Aspect(), 0), + }, + }; + const std::array write_barriers = { + vk::ImageMemoryBarrier{ + .srcAccessMask = vk::AccessFlagBits::eTransferRead, + .dstAccessMask = msaa_surface.AccessFlags(), + .oldLayout = vk::ImageLayout::eTransferSrcOptimal, + .newLayout = vk::ImageLayout::eGeneral, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = msaa_surface.Image(Type::MultiSampled), + .subresourceRange = MakeSubresourceRange(msaa_surface.Aspect(), 0), + }, + vk::ImageMemoryBarrier{ + .srcAccessMask = vk::AccessFlagBits::eTransferWrite, + .dstAccessMask = msaa_surface.AccessFlags(), + .oldLayout = vk::ImageLayout::eTransferDstOptimal, + .newLayout = vk::ImageLayout::eGeneral, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = msaa_surface.Image(), + .subresourceRange = MakeSubresourceRange(msaa_surface.Aspect(), 0), + }, + }; + + cmdbuf.pipelineBarrier(msaa_surface.PipelineStageFlags(), + vk::PipelineStageFlagBits::eTransfer, + vk::DependencyFlagBits::eByRegion, {}, {}, read_barriers); + + cmdbuf.resolveImage(msaa_surface.Image(Type::MultiSampled), + vk::ImageLayout::eTransferSrcOptimal, msaa_surface.Image(), + vk::ImageLayout::eTransferDstOptimal, resolve_area); + + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + msaa_surface.PipelineStageFlags(), + vk::DependencyFlagBits::eByRegion, {}, {}, write_barriers); + }); + }; + + // Must resolve images first + if (source.sample_count > 1) { + resolve_image(source); + } + if (dest.sample_count > 1) { + resolve_image(dest); + } + scheduler.Record([params, blit](vk::CommandBuffer cmdbuf) { const std::array source_offsets = { vk::Offset3D{static_cast(blit.src_rect.left), @@ -754,8 +852,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) { @@ -774,26 +871,40 @@ 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 (vk::SampleCountFlagBits(sample_count) > vk::SampleCountFlagBits::e1) { + 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 = sample_count != 1 ? Type::MultiSampled : res_scale != 1 ? Type::Scaled : + // Type::Base; 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()); }); } @@ -810,8 +921,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) { @@ -819,31 +929,43 @@ 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, debug_name); + 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 = sample_count != 1 ? Type::MultiSampled : res_scale != 1 ? Type::Scaled : + // Type::Base; 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; @@ -1099,12 +1221,6 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, } void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { - if (res_scale == new_scale && sample_count == new_sample_count) { - return; - } - - res_scale = new_scale; - sample_count = new_sample_count; const bool is_mutable = pixel_format == VideoCore::PixelFormat::RGBA8; @@ -1116,12 +1232,13 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { flags |= vk::ImageCreateFlagBits::eMutableFormat; } - if (res_scale > 1) { + if (res_scale != new_scale && res_scale > 1) { handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, - traits.native, traits.usage, flags, traits.aspect, false, - DebugName(true)); + traits.native, vk::SampleCountFlagBits::e1, traits.usage, + flags, traits.aspect, false, DebugName(true)); current = Type::Scaled; + res_scale = new_scale; runtime.renderpass_cache.EndRendering(); scheduler.Record( @@ -1144,8 +1261,23 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { } } - if (new_sample_count > 1) { - // Todo(wunk): Vulkan MSAA + if (sample_count != new_sample_count && sample_count > 1) { + handles[Type::MultiSampled].Create(GetScaledWidth(), GetScaledHeight(), levels, + texture_type, traits.native, + vk::SampleCountFlagBits(sample_count), traits.usage, + flags, traits.aspect, false, DebugName(true)); + // current = Type::MultiSampled; + sample_count = new_sample_count; + + 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); + }); } } @@ -1195,7 +1327,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; } @@ -1327,7 +1459,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::Current ? current : type; + auto& handle = handles[type]; if (handle.framebuffer) { return handle.framebuffer; } @@ -1337,11 +1470,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)); + 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, @@ -1458,7 +1600,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{params.sample_count} { auto& renderpass_cache = runtime.GetRenderpassCache(); if (shadow_rendering && !color) { return; @@ -1467,7 +1610,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(); @@ -1483,8 +1626,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; @@ -1497,7 +1640,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 98b140f96..f6314185b 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -80,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(); @@ -338,11 +339,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; } @@ -350,23 +351,30 @@ public: return render_pass; } + u8 Samples() const noexcept { + return sample_count; + } + u32 Scale() const noexcept { return res_scale; } 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 { From 63e25b2206ca3bfd7bca64d313995765218b85a1 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 19 Apr 2026 10:28:16 -0700 Subject: [PATCH 07/29] 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. --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 875658815..a889f77b8 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1270,8 +1270,8 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { sample_count = new_sample_count; runtime.renderpass_cache.EndRendering(); - scheduler.Record( - [raw_images = std::array{Image()}, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + 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, From e74ebf65a60c07424c5e56cc326a301ffec050cf Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 19 Apr 2026 10:49:09 -0700 Subject: [PATCH 08/29] 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. --- src/video_core/renderer_vulkan/vk_texture_runtime.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index f6314185b..18bd7cafa 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -308,7 +308,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)); @@ -323,6 +324,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; } @@ -351,14 +353,14 @@ public: return render_pass; } - u8 Samples() const noexcept { - return sample_count; - } - u32 Scale() const noexcept { return res_scale; } + u8 Samples() const noexcept { + return sample_count; + } + private: const Instance& instance; // Color, Depth, ColorMSAA, DepthMSAA From 326d658757fbb891684eea24a36ff31ae10a77f7 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 19 Apr 2026 11:02:17 -0700 Subject: [PATCH 09/29] 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/vk_texture_runtime.cpp | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index a889f77b8..4df1a2a12 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -615,81 +615,83 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, }; // Todo(wunk): Add a "dirty" flag for msaa resolves to avoid redundant image resolves - const auto resolve_image = [&](Surface& msaa_surface) { - scheduler.Record([&msaa_surface](vk::CommandBuffer cmdbuf) { + const auto resolve_image = [&](const Surface& msaa_surface) { + scheduler.Record([width = msaa_surface.GetScaledWidth(), + height = msaa_surface.GetScaledHeight(), aspect = msaa_surface.Aspect(), + access_flags = msaa_surface.AccessFlags(), + pipeline_state_flags = msaa_surface.PipelineStageFlags(), + msaa_image = msaa_surface.Image(Type::MultiSampled), + dest_image = msaa_surface.Image()](vk::CommandBuffer cmdbuf) { const vk::ImageResolve resolve_area = { .srcSubresource{ - .aspectMask = msaa_surface.Aspect(), + .aspectMask = aspect, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1, }, .srcOffset = {}, .dstSubresource{ - .aspectMask = msaa_surface.Aspect(), + .aspectMask = aspect, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1, }, .dstOffset = {}, - .extent{msaa_surface.GetScaledWidth(), msaa_surface.GetScaledHeight(), 1}, + .extent{width, height, 1}, }; const std::array read_barriers = { vk::ImageMemoryBarrier{ - .srcAccessMask = msaa_surface.AccessFlags(), + .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_surface.Image(Type::MultiSampled), - .subresourceRange = MakeSubresourceRange(msaa_surface.Aspect(), 0), + .image = msaa_image, + .subresourceRange = MakeSubresourceRange(aspect, 0), }, vk::ImageMemoryBarrier{ - .srcAccessMask = msaa_surface.AccessFlags(), + .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 = msaa_surface.Image(), - .subresourceRange = MakeSubresourceRange(msaa_surface.Aspect(), 0), + .image = dest_image, + .subresourceRange = MakeSubresourceRange(aspect, 0), }, }; const std::array write_barriers = { vk::ImageMemoryBarrier{ .srcAccessMask = vk::AccessFlagBits::eTransferRead, - .dstAccessMask = msaa_surface.AccessFlags(), + .dstAccessMask = access_flags, .oldLayout = vk::ImageLayout::eTransferSrcOptimal, .newLayout = vk::ImageLayout::eGeneral, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = msaa_surface.Image(Type::MultiSampled), - .subresourceRange = MakeSubresourceRange(msaa_surface.Aspect(), 0), + .image = msaa_image, + .subresourceRange = MakeSubresourceRange(aspect, 0), }, vk::ImageMemoryBarrier{ .srcAccessMask = vk::AccessFlagBits::eTransferWrite, - .dstAccessMask = msaa_surface.AccessFlags(), + .dstAccessMask = access_flags, .oldLayout = vk::ImageLayout::eTransferDstOptimal, .newLayout = vk::ImageLayout::eGeneral, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = msaa_surface.Image(), - .subresourceRange = MakeSubresourceRange(msaa_surface.Aspect(), 0), + .image = dest_image, + .subresourceRange = MakeSubresourceRange(aspect, 0), }, }; - cmdbuf.pipelineBarrier(msaa_surface.PipelineStageFlags(), - vk::PipelineStageFlagBits::eTransfer, + cmdbuf.pipelineBarrier(pipeline_state_flags, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlagBits::eByRegion, {}, {}, read_barriers); - cmdbuf.resolveImage(msaa_surface.Image(Type::MultiSampled), - vk::ImageLayout::eTransferSrcOptimal, msaa_surface.Image(), + cmdbuf.resolveImage(msaa_image, vk::ImageLayout::eTransferSrcOptimal, dest_image, vk::ImageLayout::eTransferDstOptimal, resolve_area); - cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, - msaa_surface.PipelineStageFlags(), + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, pipeline_state_flags, vk::DependencyFlagBits::eByRegion, {}, {}, write_barriers); }); }; From 0b12a881c9d82a2277191f0a81c601c9c65f48a4 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 19 Apr 2026 11:03:32 -0700 Subject: [PATCH 10/29] 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. --- src/video_core/rasterizer_cache/framebuffer_base.h | 3 +-- src/video_core/rasterizer_cache/rasterizer_cache.h | 1 - src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/video_core/rasterizer_cache/framebuffer_base.h b/src/video_core/rasterizer_cache/framebuffer_base.h index f7387b79f..2f2545849 100644 --- a/src/video_core/rasterizer_cache/framebuffer_base.h +++ b/src/video_core/rasterizer_cache/framebuffer_base.h @@ -27,8 +27,7 @@ struct FramebufferParams { u32 color_level; u32 depth_level; bool shadow_rendering; - u8 sample_count; - INSERT_PADDING_BYTES(2); + INSERT_PADDING_BYTES(3); bool operator==(const FramebufferParams& params) const noexcept { return std::memcmp(this, ¶ms, sizeof(FramebufferParams)) == 0; diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index fbf53205a..b87a59ecb 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -780,7 +780,6 @@ FramebufferHelper RasterizerCache::GetFramebufferSurfaces(bool using_color .color_level = color_level, .depth_level = depth_level, .shadow_rendering = regs.framebuffer.IsShadowRendering(), - .sample_count = sample_count, }; auto [it, new_framebuffer] = framebuffers.try_emplace(fb_params); diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 4df1a2a12..583b97cfb 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1603,7 +1603,7 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa Surface* color, Surface* depth) : VideoCore::FramebufferParams{params}, instance{runtime.GetInstance()}, res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)}, - sample_count{params.sample_count} { + sample_count{color ? color->sample_count : (depth ? depth->sample_count : 1u)} { auto& renderpass_cache = runtime.GetRenderpassCache(); if (shadow_rendering && !color) { return; From 216952b8cd18af77a0206221b96a4f7a6b5a4765 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 19 Apr 2026 11:41:04 -0700 Subject: [PATCH 11/29] 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/vk_texture_runtime.cpp | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 583b97cfb..171315f39 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -887,7 +887,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param } // Upscaled+MSAA image - if (vk::SampleCountFlagBits(sample_count) > vk::SampleCountFlagBits::e1) { + if (sample_count > 1) { handles[Type::MultiSampled].Create( GetScaledWidth(), GetScaledHeight(), levels, texture_type, format, vk::SampleCountFlagBits(sample_count), traits.usage, flags, traits.aspect, @@ -895,8 +895,6 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param raw_images.emplace_back(handles[Type::MultiSampled].image); } - // current = sample_count != 1 ? Type::MultiSampled : res_scale != 1 ? Type::Scaled : - // Type::Base; current = res_scale != 1 ? Type::Scaled : Type::Base; runtime.renderpass_cache.EndRendering(); @@ -956,8 +954,6 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface raw_images.emplace_back(handles[Type::Custom].image); } - // current = sample_count != 1 ? Type::MultiSampled : res_scale != 1 ? Type::Scaled : - // Type::Base; current = res_scale != 1 ? Type::Scaled : Type::Base; runtime.renderpass_cache.EndRendering(); @@ -1223,7 +1219,6 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, } void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { - const bool is_mutable = pixel_format == VideoCore::PixelFormat::RGBA8; vk::ImageCreateFlags flags{}; @@ -1234,13 +1229,15 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { flags |= vk::ImageCreateFlagBits::eMutableFormat; } + bool res_scale_modified = false; if (res_scale != new_scale && res_scale > 1) { + res_scale_modified = res_scale != new_scale; + res_scale = new_scale; 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; - res_scale = new_scale; runtime.renderpass_cache.EndRendering(); scheduler.Record( @@ -1263,23 +1260,23 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { } } - if (sample_count != new_sample_count && sample_count > 1) { + if ((res_scale_modified || sample_count != new_sample_count) && 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(true)); // current = Type::MultiSampled; - sample_count = new_sample_count; 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); - }); + std::array barriers; + MakeInitBarriers(aspect, 1, raw_images, barriers); + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, {}, {}, barriers); + }); } } From 7ce88bc63b8b591b3b7277dd5d6a8968cd096416 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 26 Apr 2026 14:27:08 -0700 Subject: [PATCH 12/29] renderer_vulkan: Fix cleanup and debug-naming for `d24s8_to_rgba8_ms_comp` --- src/video_core/renderer_vulkan/vk_blit_helper.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.cpp b/src/video_core/renderer_vulkan/vk_blit_helper.cpp index 7903d671d..dc758c13f 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_helper.cpp @@ -289,6 +289,7 @@ 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"); + SetObjectName(device, d24s8_to_rgba8_ms_comp, "BlitHelper: d24s8_to_rgba8_ms_comp"); 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"); @@ -316,6 +317,7 @@ BlitHelper::~BlitHelper() { device.destroyPipelineLayout(three_textures_pipeline_layout); device.destroyShaderModule(full_screen_vert); device.destroyShaderModule(d24s8_to_rgba8_comp); + device.destroyShaderModule(d24s8_to_rgba8_ms_comp); device.destroyShaderModule(depth_to_buffer_comp); if (blit_depth_stencil_frag) { device.destroyShaderModule(blit_depth_stencil_frag); From 5ded99d326570be1943a6bb344eda0c43ee8b94b Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 26 Apr 2026 14:48:29 -0700 Subject: [PATCH 13/29] 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/vk_texture_runtime.cpp | 187 +++++++++--------- .../renderer_vulkan/vk_texture_runtime.h | 3 + 2 files changed, 99 insertions(+), 91 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 171315f39..74dd0398c 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -492,6 +492,93 @@ void TextureRuntime::ClearTextureWithRenderpass(Surface& surface, }); } +void TextureRuntime::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 = MakeSubresourceRange(aspect, 0); + + 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); + }); +} + bool TextureRuntime::CopyTextures(Surface& source, Surface& dest, std::span copies) { renderpass_cache.EndRendering(); @@ -604,6 +691,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) { + ResolveTexture(source); + } + if (dest.sample_count > 1) { + ResolveTexture(dest); + } + const RecordParams params = { .aspect = source.Aspect(), .filter = MakeFilter(source.pixel_format), @@ -614,96 +710,6 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, .dst_image = dest.Image(), }; - // Todo(wunk): Add a "dirty" flag for msaa resolves to avoid redundant image resolves - const auto resolve_image = [&](const Surface& msaa_surface) { - scheduler.Record([width = msaa_surface.GetScaledWidth(), - height = msaa_surface.GetScaledHeight(), aspect = msaa_surface.Aspect(), - access_flags = msaa_surface.AccessFlags(), - pipeline_state_flags = msaa_surface.PipelineStageFlags(), - msaa_image = msaa_surface.Image(Type::MultiSampled), - dest_image = msaa_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, height, 1}, - }; - - 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 = MakeSubresourceRange(aspect, 0), - }, - 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 = MakeSubresourceRange(aspect, 0), - }, - }; - 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 = MakeSubresourceRange(aspect, 0), - }, - 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 = MakeSubresourceRange(aspect, 0), - }, - }; - - 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); - }); - }; - - // Must resolve images first - if (source.sample_count > 1) { - resolve_image(source); - } - if (dest.sample_count > 1) { - resolve_image(dest); - } - scheduler.Record([params, blit](vk::CommandBuffer cmdbuf) { const std::array source_offsets = { vk::Offset3D{static_cast(blit.src_rect.left), @@ -833,7 +839,6 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param scheduler{runtime_.GetScheduler()}, traits{instance.GetTraits(pixel_format)}, handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance), Handle(instance)} { - if (pixel_format == VideoCore::PixelFormat::Invalid || !traits.transfer_support) { return; } diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index 18bd7cafa..7bffdd373 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -163,6 +163,9 @@ private: /// Clears a partial texture rect using a clear rectangle void ClearTextureWithRenderpass(Surface& surface, const VideoCore::TextureClear& clear); + /// Resolves the multi-sampled texture of a surface, if available, into the current texture + void ResolveTexture(Surface& surface); + private: const Instance& instance; Scheduler& scheduler; From a73c8c7c867b51591d93f9ef8647427139096077 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 26 Apr 2026 15:00:34 -0700 Subject: [PATCH 14/29] renderer_vulkan: Fix narrowing byte conversion Fixes a compilation error on Unix platforms. --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 74dd0398c..e723d120d 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1605,7 +1605,7 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa Surface* color, Surface* depth) : VideoCore::FramebufferParams{params}, instance{runtime.GetInstance()}, res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)}, - sample_count{color ? color->sample_count : (depth ? depth->sample_count : 1u)} { + sample_count{color ? color->sample_count : (depth ? depth->sample_count : u8(1u))} { auto& renderpass_cache = runtime.GetRenderpassCache(); if (shadow_rendering && !color) { return; From e90cbba7e222849f4588547533444c05d7eb2ef9 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 26 Apr 2026 15:04:01 -0700 Subject: [PATCH 15/29] renderer_vulkan: Fix MSAA framebuffer target resolve surface Use the specified type rather than defaulting to the surface's current one(implicit `ImageView` argument). --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index e723d120d..d83c45a0a 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1477,7 +1477,7 @@ vk::Framebuffer Surface::Framebuffer(Type type) noexcept { boost::container::small_vector image_views; if (sample_count > 1) { // Main surface + MSAA surface - image_views.emplace_back(ImageView(ViewType::Mip0)); + image_views.emplace_back(ImageView(ViewType::Mip0, type)); image_views.emplace_back(ImageView(ViewType::Mip0, Type::MultiSampled)); } else { image_views.emplace_back(ImageView(ViewType::Mip0, type)); From 808cf2c7d2edcb2d0ebea40eb12afb809b7e5397 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 28 Apr 2026 21:43:05 -0700 Subject: [PATCH 16/29] 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. --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index d83c45a0a..f8e267996 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1234,9 +1234,8 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { flags |= vk::ImageCreateFlagBits::eMutableFormat; } - bool res_scale_modified = false; - if (res_scale != new_scale && res_scale > 1) { - res_scale_modified = res_scale != new_scale; + const bool res_scale_modified = res_scale != new_scale; + if (res_scale_modified && new_scale > 1) { res_scale = new_scale; handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, @@ -1265,7 +1264,7 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { } } - if ((res_scale_modified || sample_count != new_sample_count) && sample_count > 1) { + 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, From bad3313f04904a2fab08f1fe69198e868bc91f43 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 28 Apr 2026 22:30:13 -0700 Subject: [PATCH 17/29] 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? --- .../format_reinterpreter/vulkan_d24s8_to_rgba8_ms.comp | 4 ++-- src/video_core/renderer_vulkan/vk_blit_helper.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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 index 3dca7c91e..62297a202 100644 --- 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 @@ -17,10 +17,10 @@ layout(push_constant, std140) uniform ComputeInfo { }; void main() { - int sample_count = textureSamples(depth); - 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)); diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.cpp b/src/video_core/renderer_vulkan/vk_blit_helper.cpp index dc758c13f..0d530ac95 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_helper.cpp @@ -421,12 +421,12 @@ bool BlitHelper::ConvertDS24S8ToRGBA8(Surface& source, Surface& dest, update_queue.AddImageSampler(descriptor_set, 1, 0, source.ImageView(ViewType::Stencil, src_type), 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, pipeline, 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, From 7eac2ec7af4149de3b9e9e6070305361c9001696 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 28 Apr 2026 23:31:40 -0700 Subject: [PATCH 18/29] 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. --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index f8e267996..d9b2d1ff2 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -425,8 +425,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 From 844939d372e32759db3ff0845ac74913e3b4f211 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 28 Apr 2026 23:32:53 -0700 Subject: [PATCH 19/29] 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. --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index d9b2d1ff2..79fa183ae 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1465,7 +1465,7 @@ vk::ImageView Surface::ImageView(ViewType view_type, Type type) noexcept { } vk::Framebuffer Surface::Framebuffer(Type type) noexcept { - type = Type::Current ? current : type; + type = (type == Type::Current) ? current : type; auto& handle = handles[type]; if (handle.framebuffer) { return handle.framebuffer; @@ -1479,7 +1479,7 @@ vk::Framebuffer Surface::Framebuffer(Type type) noexcept { boost::container::small_vector image_views; if (sample_count > 1) { // Main surface + MSAA surface - image_views.emplace_back(ImageView(ViewType::Mip0, type)); + 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)); From 4c4c75cd1b46270ca3c08019c7b34a738870da6c Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Wed, 29 Apr 2026 00:00:06 -0700 Subject: [PATCH 20/29] renderer_vulkan: Fix MSAA image debug name --- .../renderer_vulkan/vk_texture_runtime.cpp | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 79fa183ae..1bbbe9969 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -949,10 +949,10 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface 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, debug_name); + 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) { @@ -1269,10 +1269,14 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { 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(true)); + 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(); From a922cdaa451129d7c7c526e70d4f885a89b7fa76 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Wed, 29 Apr 2026 00:25:25 -0700 Subject: [PATCH 21/29] 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! --- .../renderer_vulkan/vk_blit_helper.cpp | 98 +++++++++++++++++++ .../renderer_vulkan/vk_blit_helper.h | 2 + .../renderer_vulkan/vk_texture_runtime.cpp | 91 +---------------- .../renderer_vulkan/vk_texture_runtime.h | 3 - 4 files changed, 102 insertions(+), 92 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.cpp b/src/video_core/renderer_vulkan/vk_blit_helper.cpp index 0d530ac95..e4fc489ef 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_helper.cpp @@ -523,6 +523,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; } @@ -601,6 +606,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 16e0090b0..e9ea63cac 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.h +++ b/src/video_core/renderer_vulkan/vk_blit_helper.h @@ -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(); diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 1bbbe9969..9420bcfb7 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -495,93 +495,6 @@ void TextureRuntime::ClearTextureWithRenderpass(Surface& surface, }); } -void TextureRuntime::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 = MakeSubresourceRange(aspect, 0); - - 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); - }); -} - bool TextureRuntime::CopyTextures(Surface& source, Surface& dest, std::span copies) { renderpass_cache.EndRendering(); @@ -697,10 +610,10 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, // Must resolve images first // Todo(wunk): Add a "dirty" flag for msaa resolves to avoid redundant image resolves if (source.sample_count > 1) { - ResolveTexture(source); + blit_helper.ResolveTexture(source); } if (dest.sample_count > 1) { - ResolveTexture(dest); + blit_helper.ResolveTexture(dest); } const RecordParams params = { diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index 7bffdd373..18bd7cafa 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -163,9 +163,6 @@ private: /// Clears a partial texture rect using a clear rectangle void ClearTextureWithRenderpass(Surface& surface, const VideoCore::TextureClear& clear); - /// Resolves the multi-sampled texture of a surface, if available, into the current texture - void ResolveTexture(Surface& surface); - private: const Instance& instance; Scheduler& scheduler; From 63a52b9dc334dfe9c2f9f6894a82bd9563967548 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 9 May 2026 17:58:45 -0700 Subject: [PATCH 22/29] config: Refactor `sample_count` to `antialiasing` setting Intended to allow for other anti-aliasing methods to be introduced --- CMakeModules/GenerateSettingKeys.cmake | 1 + src/android/app/src/main/jni/config.cpp | 2 +- src/citra_qt/configuration/config.cpp | 4 ++-- .../configuration/configure_enhancements.cpp | 20 ++++++++--------- .../configuration/configure_enhancements.ui | 16 +++++++------- src/common/settings.cpp | 4 ++-- src/common/settings.h | 22 ++++++++++++++++++- src/video_core/renderer_base.cpp | 2 +- .../renderer_vulkan/vk_rasterizer.cpp | 3 ++- 9 files changed, 48 insertions(+), 26 deletions(-) diff --git a/CMakeModules/GenerateSettingKeys.cmake b/CMakeModules/GenerateSettingKeys.cmake index 7aff65db5..c3074b36d 100644 --- a/CMakeModules/GenerateSettingKeys.cmake +++ b/CMakeModules/GenerateSettingKeys.cmake @@ -44,6 +44,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 7411f1e62..874a0f605 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -150,7 +150,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.sample_count); + ReadSetting("Renderer", Settings::values.antialiasing); ReadSetting("Renderer", Settings::values.use_disk_shader_cache); ReadSetting("Renderer", Settings::values.use_vsync); ReadSetting("Renderer", Settings::values.texture_filter); diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index b545f0424..d57defb0f 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -716,7 +716,7 @@ void QtConfig::ReadRendererValues() { ReadGlobalSetting(Settings::values.use_vsync); ReadGlobalSetting(Settings::values.use_display_refresh_rate_detection); ReadGlobalSetting(Settings::values.resolution_factor); - ReadGlobalSetting(Settings::values.sample_count); + ReadGlobalSetting(Settings::values.antialiasing); ReadGlobalSetting(Settings::values.use_integer_scaling); ReadGlobalSetting(Settings::values.frame_limit); ReadGlobalSetting(Settings::values.turbo_limit); @@ -1266,7 +1266,7 @@ void QtConfig::SaveRendererValues() { WriteGlobalSetting(Settings::values.use_vsync); WriteGlobalSetting(Settings::values.use_display_refresh_rate_detection); WriteGlobalSetting(Settings::values.resolution_factor); - WriteGlobalSetting(Settings::values.sample_count); + 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 01bd0cb8a..4d2d383f6 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -22,7 +22,7 @@ ConfigureEnhancements::ConfigureEnhancements(QWidget* parent) const bool res_scale_enabled = graphics_api != Settings::GraphicsAPI::Software; ui->resolution_factor_combobox->setEnabled(res_scale_enabled); const bool msaa_enabled = graphics_api == Settings::GraphicsAPI::Vulkan; - ui->sample_count_combobox->setEnabled(msaa_enabled); + ui->antialiasing_combobox->setEnabled(msaa_enabled); connect(ui->render_3d_combobox, qOverload(&QComboBox::currentIndexChanged), this, [this](int currentIndex) { @@ -46,8 +46,8 @@ void ConfigureEnhancements::SetConfiguration() { if (!Settings::IsConfiguringGlobal()) { ConfigurationShared::SetPerGameSetting(ui->resolution_factor_combobox, &Settings::values.resolution_factor); - ConfigurationShared::SetPerGameSetting(ui->sample_count_combobox, - &Settings::values.sample_count); + ConfigurationShared::SetPerGameSetting(ui->antialiasing_combobox, + &Settings::values.antialiasing); ConfigurationShared::SetPerGameSetting(ui->texture_filter_combobox, &Settings::values.texture_filter); ConfigurationShared::SetHighlight(ui->widget_texture_filter, @@ -55,8 +55,8 @@ void ConfigureEnhancements::SetConfiguration() { } else { ui->resolution_factor_combobox->setCurrentIndex( Settings::values.resolution_factor.GetValue()); - ui->sample_count_combobox->setCurrentIndex( - static_cast(Settings::values.sample_count.GetValue())); + ui->antialiasing_combobox->setCurrentIndex( + static_cast(Settings::values.antialiasing.GetValue())); ui->texture_filter_combobox->setCurrentIndex( static_cast(Settings::values.texture_filter.GetValue())); } @@ -117,8 +117,8 @@ void ConfigureEnhancements::RetranslateUI() { void ConfigureEnhancements::ApplyConfiguration() { ConfigurationShared::ApplyPerGameSetting(&Settings::values.resolution_factor, ui->resolution_factor_combobox); - ConfigurationShared::ApplyPerGameSetting(&Settings::values.sample_count, - ui->sample_count_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(); @@ -157,7 +157,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_sample_count->setEnabled(Settings::values.sample_count.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()); @@ -199,8 +199,8 @@ void ConfigureEnhancements::SetupPerGameUI() { static_cast(Settings::values.resolution_factor.GetValue(true))); ConfigurationShared::SetColoredComboBox( - ui->sample_count_combobox, ui->widget_sample_count, - static_cast(Settings::values.sample_count.GetValue(true))); + ui->antialiasing_combobox, ui->widget_antialiasing, + static_cast(Settings::values.antialiasing.GetValue(true))); ConfigurationShared::SetColoredComboBox( ui->texture_filter_combobox, ui->widget_texture_filter, diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui index f040fb7ce..833be5717 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -111,7 +111,7 @@ - + 0 @@ -126,32 +126,32 @@ 0 - + - Sample count + Anti-Aliasing - + - Native (x1) + None - x2 + MSAAx2 - x4 + MSAAx4 - x8 + MSAAx8 diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 56a6200c9..747da6724 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -97,7 +97,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.sample_count.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()); @@ -214,7 +214,7 @@ void RestoreGlobalState(bool is_powered_on) { values.shaders_accurate_mul.SetGlobal(true); values.use_vsync.SetGlobal(true); values.resolution_factor.SetGlobal(true); - values.sample_count.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 b0d0ccb9e..6d93f4e14 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -103,6 +103,26 @@ enum class AudioEmulation : u32 { LLEMultithreaded = 2, }; +enum class AntiAliasingMethod : u32 { + None = 0, + MSAAx2 = 1, + MSAAx4 = 2, + MSAAx8 = 3, +}; + +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; + default: + return 1; + } +} + enum class TextureFilter : u32 { NoFilter = 0, Anime4K = 1, @@ -535,7 +555,7 @@ struct Values { true, Keys::use_display_refresh_rate_detection}; Setting use_shader_jit{true, Keys::use_shader_jit}; SwitchableSetting resolution_factor{1, 0, 10, Keys::resolution_factor}; - SwitchableSetting sample_count{0, 0, 3, "sample_count"}; + 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/renderer_base.cpp b/src/video_core/renderer_base.cpp index 8331664da..d81a031e9 100644 --- a/src/video_core/renderer_base.cpp +++ b/src/video_core/renderer_base.cpp @@ -37,7 +37,7 @@ u8 RendererBase::GetSampleCount() const { return 1; } - return static_cast(1u << Settings::values.sample_count.GetValue()); + return Settings::GetAntiAliasingSampleCount(Settings::values.antialiasing.GetValue()); } void RendererBase::UpdateCurrentFramebufferLayout(bool is_portrait_mode) { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 0fb9ab449..2f995d270 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -778,7 +778,8 @@ bool RasterizerVulkan::AccelerateDisplay(const Pica::FramebufferConfig& config, src_params.stride = pixel_stride; src_params.is_tiled = false; src_params.pixel_format = VideoCore::PixelFormatFromGPUPixelFormat(config.color_format); - src_params.sample_count = (1u << Settings::values.sample_count.GetValue()); + src_params.sample_count = + Settings::GetAntiAliasingSampleCount(Settings::values.antialiasing.GetValue()); src_params.UpdateParams(); const auto [src_surface_id, src_rect] = From 86c05a27e84c4c43e84bb8cd748189e741c444e3 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 9 May 2026 20:24:34 -0700 Subject: [PATCH 23/29] 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` --- .../configuration/configure_enhancements.cpp | 7 +- src/video_core/renderer_base.cpp | 7 - .../renderer_opengl/gl_blit_helper.cpp | 17 +++ .../renderer_opengl/gl_blit_helper.h | 5 +- .../renderer_opengl/gl_rasterizer.cpp | 4 + .../renderer_opengl/gl_texture_runtime.cpp | 124 +++++++++++++----- .../renderer_opengl/gl_texture_runtime.h | 11 +- 7 files changed, 130 insertions(+), 45 deletions(-) diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp index 4d2d383f6..5075ddf94 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -19,10 +19,9 @@ ConfigureEnhancements::ConfigureEnhancements(QWidget* parent) SetConfiguration(); const auto graphics_api = Settings::values.graphics_api.GetValue(); - const bool res_scale_enabled = graphics_api != Settings::GraphicsAPI::Software; - ui->resolution_factor_combobox->setEnabled(res_scale_enabled); - const bool msaa_enabled = graphics_api == Settings::GraphicsAPI::Vulkan; - ui->antialiasing_combobox->setEnabled(msaa_enabled); + const bool hardware_graphics = graphics_api != Settings::GraphicsAPI::Software; + ui->resolution_factor_combobox->setEnabled(hardware_graphics); + ui->antialiasing_combobox->setEnabled(hardware_graphics); connect(ui->render_3d_combobox, qOverload(&QComboBox::currentIndexChanged), this, [this](int currentIndex) { diff --git a/src/video_core/renderer_base.cpp b/src/video_core/renderer_base.cpp index d81a031e9..916102e1f 100644 --- a/src/video_core/renderer_base.cpp +++ b/src/video_core/renderer_base.cpp @@ -30,13 +30,6 @@ u32 RendererBase::GetResolutionScaleFactor() { } u8 RendererBase::GetSampleCount() const { - const auto graphics_api = Settings::values.graphics_api.GetValue(); - - // Enabled for vulkan only for now - if (graphics_api != Settings::GraphicsAPI::Vulkan) { - return 1; - } - return Settings::GetAntiAliasingSampleCount(Settings::values.antialiasing.GetValue()); } diff --git a/src/video_core/renderer_opengl/gl_blit_helper.cpp b/src/video_core/renderer_opengl/gl_blit_helper.cpp index 30a502316..1d1a66150 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.cpp +++ b/src/video_core/renderer_opengl/gl_blit_helper.cpp @@ -67,6 +67,7 @@ BlitHelper::BlitHelper(const Driver& driver_) 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")} { vao.Create(); + read_fbo.Create(); draw_fbo.Create(); state.draw.vertex_array = vao.handle; for (u32 i = 0; i < 3; i++) { @@ -156,6 +157,22 @@ bool BlitHelper::ConvertRGBA4ToRGB5A1(Surface& source, Surface& dest, return true; } +void BlitHelper::ResolveTexture(Surface& surface) { + + state.draw.read_framebuffer = read_fbo.handle; + state.draw.draw_framebuffer = draw_fbo.handle; + state.Apply(); + + surface.Attach(GL_READ_FRAMEBUFFER, 0, 0, 3); + surface.Attach(GL_DRAW_FRAMEBUFFER, 0, 0, 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 = diff --git a/src/video_core/renderer_opengl/gl_blit_helper.h b/src/video_core/renderer_opengl/gl_blit_helper.h index 054fdef84..bc4047456 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.h +++ b/src/video_core/renderer_opengl/gl_blit_helper.h @@ -1,4 +1,4 @@ -// 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. @@ -31,6 +31,8 @@ public: bool ConvertRGBA4ToRGB5A1(Surface& source, Surface& dest, const VideoCore::TextureCopy& copy); + void ResolveTexture(Surface& surface); + private: void FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit); void FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit); @@ -47,6 +49,7 @@ private: const Driver& driver; OGLVertexArray vao; OpenGLState state; + OGLFramebuffer read_fbo; OGLFramebuffer draw_fbo; OGLSampler linear_sampler; OGLSampler nearest_sampler; diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 407e2a5c0..50536da02 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; diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index 4ce41a0fc..863f00f82 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -92,13 +92,20 @@ 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, false); + } 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); @@ -215,6 +222,14 @@ bool TextureRuntime::ClearTextureWithoutFbo(Surface& surface, glClearTexSubImage(surface.Handle(), clear.texture_level, clear.texture_rect.left, clear.texture_rect.bottom, 0, 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, 0, clear.texture_rect.GetWidth(), + clear.texture_rect.GetHeight(), 1, format, type, &clear.value); + } + return true; } @@ -279,6 +294,15 @@ bool TextureRuntime::CopyTextures(Surface& source, Surface& dest, bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, const VideoCore::TextureBlit& blit) { + // 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); + } + OpenGLState state = OpenGLState::GetCurState(); state.scissor.enabled = false; state.draw.read_framebuffer = read_fbos[FboIndex(source.type)].handle; @@ -329,11 +353,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, @@ -351,15 +380,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)); } } @@ -374,8 +407,8 @@ GLuint Surface::Handle(u32 index) const noexcept { GLuint Surface::CopyHandle() noexcept { 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++) { @@ -534,22 +567,26 @@ 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!"); @@ -557,17 +594,11 @@ void Surface::Attach(GLenum target, u32 level, u32 layer, bool scaled) { } void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { - if (res_scale == new_scale && sample_count == new_sample_count) { - return; - } + const bool res_scale_modified = res_scale != new_scale; + if (res_scale_modified && new_scale > 1) { - res_scale = new_scale; - sample_count = new_sample_count; - - if (res_scale > 1) { - - textures[1] = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, tuple, - DebugName(true)); + 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, @@ -579,8 +610,10 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { } } - if (new_sample_count > 1) { + if ((res_scale_modified || sample_count != new_sample_count) && new_sample_count > 1) { // Todo(wunk): OpenGL MSAA + textures[3] = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, + sample_count, tuple, DebugName(true)); } } @@ -614,7 +647,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; @@ -627,6 +661,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(); @@ -658,6 +701,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 434a3c464..c5120a392 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.h +++ b/src/video_core/renderer_opengl/gl_texture_runtime.h @@ -130,7 +130,7 @@ 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 and sample-count. void ScaleUp(u32 new_scale, u8 new_sample_count); @@ -149,7 +149,7 @@ private: private: const Driver* driver; TextureRuntime* runtime; - std::array textures; + std::array textures; OGLTexture copy_texture; FormatTuple tuple; }; @@ -170,6 +170,10 @@ public: return res_scale; } + [[nodiscard]] u32 Samples() const noexcept { + return sample_count; + } + [[nodiscard]] GLuint Handle() const noexcept { return framebuffer.handle; } @@ -184,7 +188,8 @@ public: private: u32 res_scale{1}; - std::array attachments{}; + u32 sample_count{1}; + std::array attachments{}; OGLFramebuffer framebuffer; }; From 9660492f99b6a116abde8656b0de80159a829760 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 9 May 2026 22:39:12 -0700 Subject: [PATCH 24/29] renderer_gl: Fix surface `ScaleUp` implementation Should assign the new `res_scale` and `sample_count` --- src/video_core/renderer_opengl/gl_texture_runtime.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index 863f00f82..c18e16609 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -596,7 +596,7 @@ void Surface::Attach(GLenum target, u32 level, u32 layer, u32 handle) { void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { 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++) { @@ -611,7 +611,7 @@ void Surface::ScaleUp(u32 new_scale, u8 new_sample_count) { } if ((res_scale_modified || sample_count != new_sample_count) && new_sample_count > 1) { - // Todo(wunk): OpenGL MSAA + sample_count = new_sample_count; textures[3] = MakeHandle(GL_TEXTURE_2D, GetScaledWidth(), GetScaledHeight(), levels, sample_count, tuple, DebugName(true)); } From d78f719bf772c70f8518da482c2f0041c62a9e95 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 9 May 2026 23:24:38 -0700 Subject: [PATCH 25/29] renderer_gl: Implement multisample `Convert{DS24S8ToRGBA8,RGBA4ToRGB5A1}` --- src/video_core/host_shaders/CMakeLists.txt | 2 + .../d24s8_to_rgba8_ms.frag | 25 ++++++ .../rgba4_to_rgb5a1_ms.frag | 22 +++++ .../renderer_opengl/gl_blit_helper.cpp | 86 ++++++++++++++----- .../renderer_opengl/gl_blit_helper.h | 4 +- .../renderer_opengl/gl_texture_runtime.cpp | 2 +- 6 files changed, 118 insertions(+), 23 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 diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 8018a9c55..939c06c9f 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -4,7 +4,9 @@ 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 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..ae193d93a --- /dev/null +++ b/src/video_core/host_shaders/format_reinterpreter/d24s8_to_rgba8_ms.frag @@ -0,0 +1,25 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 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..ce2bef2c7 --- /dev/null +++ b/src/video_core/host_shaders/format_reinterpreter/rgba4_to_rgb5a1_ms.frag @@ -0,0 +1,22 @@ +// Copyright 2023 Citra Emulator Project +// Licensed under GPLv2 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/renderer_opengl/gl_blit_helper.cpp b/src/video_core/renderer_opengl/gl_blit_helper.cpp index 1d1a66150..378d29fdf 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.cpp +++ b/src/video_core/renderer_opengl/gl_blit_helper.cpp @@ -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,7 +67,11 @@ 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(); @@ -88,46 +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(); + 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 = 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 = 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(); @@ -137,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); + } + return true; } @@ -145,22 +173,36 @@ 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(); + 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); + } return true; } void BlitHelper::ResolveTexture(Surface& surface) { + 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, 0, 0, 3); @@ -307,7 +349,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; @@ -316,9 +358,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 bc4047456..7eab60600 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.h +++ b/src/video_core/renderer_opengl/gl_blit_helper.h @@ -43,7 +43,7 @@ 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; @@ -62,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_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index c18e16609..75b04dab9 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -101,7 +101,7 @@ static constexpr std::array CUSTOM_TUPLES = {{ ASSERT(target == GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture.handle); glTexStorage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, tuple.internal_format, width, - height, false); + height, true); } else { glBindTexture(target, texture.handle); glTexStorage2D(target, levels, tuple.internal_format, width, height); From 71179b1e9c9a12377688b90e23c2955de804600b Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 16 May 2026 23:42:36 -0700 Subject: [PATCH 26/29] 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_opengl/gl_rasterizer.cpp | 19 ++++++++++++++++++- .../renderer_opengl/gl_texture_runtime.h | 5 +++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 50536da02..3f76f74e1 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -647,6 +647,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); + } + } + + if (framebuffer->depth_id != VideoCore::SurfaceId{}) { + Surface& depth_surface = res_cache.GetSurface(framebuffer->depth_id); + if (depth_surface.GetSampleCount() > 1) { + runtime.ResolveTexture(depth_surface); + } + } + vertex_batch.clear(); if (shadow_rendering) { @@ -1009,7 +1025,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.h b/src/video_core/renderer_opengl/gl_texture_runtime.h index c5120a392..acd922578 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.h +++ b/src/video_core/renderer_opengl/gl_texture_runtime.h @@ -78,6 +78,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) { + blit_helper.ResolveTexture(surface); + } + private: /// Returns the OpenGL driver class const Driver& GetDriver() const { From 31c9b8d4a9814649c7b337429b1668f81049df2d Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 26 May 2026 07:52:50 -0700 Subject: [PATCH 27/29] renderer_gl: Implement `TextureRuntime::ClearTexture` MSAA clears When ArbClearTexture is not available, ensure the fallback implementation also clears the MSAA texture as well. --- .../renderer_opengl/gl_texture_runtime.cpp | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index 75b04dab9..982e227cd 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -247,31 +247,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, 0); + 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, 0); + ClearBuffer(); + + if (surface.GetSampleCount() > 1) { + // Clear MSAA too + surface.Attach(GL_DRAW_FRAMEBUFFER, clear.texture_level, 0, 3); + ClearBuffer(); } } From 773c086dca5c45821853bbb4a9c22794a6069054 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 26 May 2026 09:21:26 -0700 Subject: [PATCH 28/29] renderer_gl: Fix MakeHandle string-view hazard `glObjectLabel` expects a null-terminated string, but an `std::string_view` is not necessarily null-terminated --- src/video_core/renderer_opengl/gl_texture_runtime.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index 982e227cd..09450f2b1 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -112,7 +112,7 @@ static constexpr std::array CUSTOM_TUPLES = {{ 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; From 6b62f8ec193af59316176172205cacb623773666 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Tue, 26 May 2026 20:04:26 -0700 Subject: [PATCH 29/29] renderer_gl: Add layer/level specifier to MSAA resolves --- src/video_core/renderer_opengl/gl_blit_helper.cpp | 10 +++++----- src/video_core/renderer_opengl/gl_blit_helper.h | 2 +- src/video_core/renderer_opengl/gl_rasterizer.cpp | 4 ++-- src/video_core/renderer_opengl/gl_texture_runtime.cpp | 4 ++-- src/video_core/renderer_opengl/gl_texture_runtime.h | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/video_core/renderer_opengl/gl_blit_helper.cpp b/src/video_core/renderer_opengl/gl_blit_helper.cpp index 378d29fdf..be399bddb 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.cpp +++ b/src/video_core/renderer_opengl/gl_blit_helper.cpp @@ -162,7 +162,7 @@ bool BlitHelper::ConvertDS24S8ToRGBA8(Surface& source, Surface& dest, if (multisample) { // Resolve the destination image if needed - ResolveTexture(dest); + ResolveTexture(dest, copy.dst_level, copy.dst_layer); } return true; @@ -188,13 +188,13 @@ bool BlitHelper::ConvertRGBA4ToRGB5A1(Surface& source, Surface& dest, if (multisample) { // Resolve the destination image if needed - ResolveTexture(dest); + ResolveTexture(dest, copy.dst_level, copy.dst_layer); } return true; } -void BlitHelper::ResolveTexture(Surface& surface) { +void BlitHelper::ResolveTexture(Surface& surface, u32 level, u32 layer) { OpenGLState prev_state = OpenGLState::GetCurState(); SCOPE_EXIT({ prev_state.Apply(); }); @@ -205,8 +205,8 @@ void BlitHelper::ResolveTexture(Surface& surface) { state.texture_units[2].texture_2d = 0; state.Apply(); - surface.Attach(GL_READ_FRAMEBUFFER, 0, 0, 3); - surface.Attach(GL_DRAW_FRAMEBUFFER, 0, 0, 1); + 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) diff --git a/src/video_core/renderer_opengl/gl_blit_helper.h b/src/video_core/renderer_opengl/gl_blit_helper.h index 7eab60600..e1b9c77ed 100644 --- a/src/video_core/renderer_opengl/gl_blit_helper.h +++ b/src/video_core/renderer_opengl/gl_blit_helper.h @@ -31,7 +31,7 @@ public: bool ConvertRGBA4ToRGB5A1(Surface& source, Surface& dest, const VideoCore::TextureCopy& copy); - void ResolveTexture(Surface& surface); + void ResolveTexture(Surface& surface, u32 level = 0, u32 layer = 0); private: void FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit); diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 3f76f74e1..119fcec1b 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -652,14 +652,14 @@ bool RasterizerOpenGL::Draw(bool accelerate, bool is_indexed) { if (framebuffer->color_id != VideoCore::SurfaceId{}) { Surface& color_surface = res_cache.GetSurface(framebuffer->color_id); if (color_surface.GetSampleCount() > 1) { - runtime.ResolveTexture(color_surface); + 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); + runtime.ResolveTexture(depth_surface, framebuffer->depth_level); } } diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.cpp b/src/video_core/renderer_opengl/gl_texture_runtime.cpp index 09450f2b1..65be0e788 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.cpp +++ b/src/video_core/renderer_opengl/gl_texture_runtime.cpp @@ -306,10 +306,10 @@ bool TextureRuntime::BlitTextures(Surface& source, Surface& dest, // 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_helper.ResolveTexture(source, blit.src_level, blit.src_layer); } if (dest.sample_count > 1) { - blit_helper.ResolveTexture(dest); + blit_helper.ResolveTexture(dest, blit.dst_level, blit.dst_layer); } OpenGLState state = OpenGLState::GetCurState(); diff --git a/src/video_core/renderer_opengl/gl_texture_runtime.h b/src/video_core/renderer_opengl/gl_texture_runtime.h index acd922578..9461aeb8d 100644 --- a/src/video_core/renderer_opengl/gl_texture_runtime.h +++ b/src/video_core/renderer_opengl/gl_texture_runtime.h @@ -79,8 +79,8 @@ public: void GenerateMipmaps(Surface& surface); /// Resolve a surface's MSAA texture into the surface's appropriate non-MSAA texture - void ResolveTexture(Surface& surface) { - blit_helper.ResolveTexture(surface); + void ResolveTexture(Surface& surface, u32 level = 0, u32 layer = 0) { + blit_helper.ResolveTexture(surface, level, layer); } private: