diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.cpp b/src/video_core/renderer_vulkan/vk_blit_helper.cpp index 0a7a3be44..679ab3dcd 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_helper.cpp @@ -1,7 +1,8 @@ -// 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. +#include "common/settings.h" #include "common/vector_math.h" #include "video_core/renderer_vulkan/vk_blit_helper.h" #include "video_core/renderer_vulkan/vk_descriptor_update_queue.h" @@ -16,8 +17,19 @@ #include "video_core/host_shaders/vulkan_blit_depth_stencil_frag.h" #include "video_core/host_shaders/vulkan_depth_to_buffer_comp.h" +// Texture filtering shader includes +#include "video_core/host_shaders/texture_filtering/bicubic_frag.h" +#include "video_core/host_shaders/texture_filtering/mmpx_frag.h" +#include "video_core/host_shaders/texture_filtering/refine_frag.h" +#include "video_core/host_shaders/texture_filtering/scale_force_frag.h" +#include "video_core/host_shaders/texture_filtering/x_gradient_frag.h" +#include "video_core/host_shaders/texture_filtering/xbrz_freescale_frag.h" +#include "video_core/host_shaders/texture_filtering/y_gradient_frag.h" +#include "vk_blit_helper.h" + namespace Vulkan { +using Settings::TextureFilter; using VideoCore::PixelFormat; namespace { @@ -55,8 +67,33 @@ constexpr std::array TWO_TEXTURES_BINDINGS = {1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, }}; +// Texture filtering descriptor set bindings +constexpr std::array SINGLE_TEXTURE_BINDINGS = {{ + {0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, +}}; + +constexpr std::array THREE_TEXTURES_BINDINGS = {{ + {0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, + {1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, + {2, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, +}}; + +// Note: Removed FILTER_UTILITY_BINDINGS as texture filtering doesn't need shadow buffers + +// Push constant structure for texture filtering +struct FilterPushConstants { + std::array tex_scale; + std::array tex_offset; + float res_scale; // For xBRZ filter +}; + +inline constexpr vk::PushConstantRange FILTER_PUSH_CONSTANT_RANGE{ + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(FilterPushConstants), +}; inline constexpr vk::PushConstantRange PUSH_CONSTANT_RANGE{ - .stageFlags = vk::ShaderStageFlagBits::eVertex, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, .offset = 0, .size = sizeof(PushConstants), }; @@ -104,12 +141,17 @@ constexpr vk::PipelineDynamicStateCreateInfo PIPELINE_DYNAMIC_STATE_CREATE_INFO{ .dynamicStateCount = static_cast(DYNAMIC_STATES.size()), .pDynamicStates = DYNAMIC_STATES.data(), }; -constexpr vk::PipelineColorBlendStateCreateInfo PIPELINE_COLOR_BLEND_STATE_EMPTY_CREATE_INFO{ + +constexpr vk::PipelineColorBlendAttachmentState COLOR_BLEND_ATTACHMENT{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA, +}; + +constexpr vk::PipelineColorBlendStateCreateInfo PIPELINE_COLOR_BLEND_STATE_CREATE_INFO{ .logicOpEnable = VK_FALSE, - .logicOp = vk::LogicOp::eClear, - .attachmentCount = 0, - .pAttachments = nullptr, - .blendConstants = std::array{0.0f, 0.0f, 0.0f, 0.0f}, + .attachmentCount = 1, + .pAttachments = &COLOR_BLEND_ATTACHMENT, }; constexpr vk::PipelineDepthStencilStateCreateInfo PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO{ .depthTestEnable = VK_TRUE, @@ -128,9 +170,9 @@ inline constexpr vk::SamplerCreateInfo SAMPLER_CREATE_INFO{ .magFilter = filter, .minFilter = filter, .mipmapMode = vk::SamplerMipmapMode::eNearest, - .addressModeU = vk::SamplerAddressMode::eClampToBorder, - .addressModeV = vk::SamplerAddressMode::eClampToBorder, - .addressModeW = vk::SamplerAddressMode::eClampToBorder, + .addressModeU = vk::SamplerAddressMode::eClampToEdge, + .addressModeV = vk::SamplerAddressMode::eClampToEdge, + .addressModeW = vk::SamplerAddressMode::eClampToEdge, .mipLodBias = 0.0f, .anisotropyEnable = VK_FALSE, .maxAnisotropy = 0.0f, @@ -143,12 +185,14 @@ inline constexpr vk::SamplerCreateInfo SAMPLER_CREATE_INFO{ }; constexpr vk::PipelineLayoutCreateInfo PipelineLayoutCreateInfo( - const vk::DescriptorSetLayout* set_layout, bool compute = false) { + const vk::DescriptorSetLayout* set_layout, bool compute = false, bool filter = false) { return vk::PipelineLayoutCreateInfo{ .setLayoutCount = 1, .pSetLayouts = set_layout, .pushConstantRangeCount = 1, - .pPushConstantRanges = (compute ? &COMPUTE_PUSH_CONSTANT_RANGE : &PUSH_CONSTANT_RANGE), + .pPushConstantRanges = + (compute ? &COMPUTE_PUSH_CONSTANT_RANGE + : (filter ? &FILTER_PUSH_CONSTANT_RANGE : &PUSH_CONSTANT_RANGE)), }; } @@ -185,12 +229,20 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, compute_provider{instance, scheduler.GetMasterSemaphore(), COMPUTE_BINDINGS}, compute_buffer_provider{instance, scheduler.GetMasterSemaphore(), COMPUTE_BUFFER_BINDINGS}, two_textures_provider{instance, scheduler.GetMasterSemaphore(), TWO_TEXTURES_BINDINGS, 16}, + single_texture_provider{instance, scheduler.GetMasterSemaphore(), SINGLE_TEXTURE_BINDINGS, + 16}, + three_textures_provider{instance, scheduler.GetMasterSemaphore(), THREE_TEXTURES_BINDINGS, + 16}, compute_pipeline_layout{ device.createPipelineLayout(PipelineLayoutCreateInfo(&compute_provider.Layout(), true))}, compute_buffer_pipeline_layout{device.createPipelineLayout( PipelineLayoutCreateInfo(&compute_buffer_provider.Layout(), true))}, two_textures_pipeline_layout{ device.createPipelineLayout(PipelineLayoutCreateInfo(&two_textures_provider.Layout()))}, + single_texture_pipeline_layout{device.createPipelineLayout( + PipelineLayoutCreateInfo(&single_texture_provider.Layout(), false, true))}, + three_textures_pipeline_layout{device.createPipelineLayout( + PipelineLayoutCreateInfo(&three_textures_provider.Layout(), false, true))}, full_screen_vert{Compile(HostShaders::FULL_SCREEN_TRIANGLE_VERT, vk::ShaderStageFlagBits::eVertex, device)}, d24s8_to_rgba8_comp{Compile(HostShaders::VULKAN_D24S8_TO_RGBA8_COMP, @@ -199,10 +251,24 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, vk::ShaderStageFlagBits::eCompute, device)}, blit_depth_stencil_frag{Compile(HostShaders::VULKAN_BLIT_DEPTH_STENCIL_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + // Texture filtering shader modules + bicubic_frag{Compile(HostShaders::BICUBIC_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + scale_force_frag{ + Compile(HostShaders::SCALE_FORCE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + xbrz_frag{ + Compile(HostShaders::XBRZ_FREESCALE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + 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)}, depth_to_buffer_pipeline{ MakeComputePipeline(depth_to_buffer_comp, compute_buffer_pipeline_layout)}, depth_blit_pipeline{MakeDepthStencilBlitPipeline()}, + // Texture filtering pipelines + bicubic_pipeline{MakeFilterPipeline(bicubic_frag, single_texture_pipeline_layout)}, + scale_force_pipeline{MakeFilterPipeline(scale_force_frag, single_texture_pipeline_layout)}, + xbrz_pipeline{MakeFilterPipeline(xbrz_frag, single_texture_pipeline_layout)}, + mmpx_pipeline{MakeFilterPipeline(mmpx_frag, single_texture_pipeline_layout)}, + refine_pipeline{MakeFilterPipeline(refine_frag, three_textures_pipeline_layout)}, linear_sampler{device.createSampler(SAMPLER_CREATE_INFO)}, nearest_sampler{device.createSampler(SAMPLER_CREATE_INFO)} { @@ -230,19 +296,33 @@ BlitHelper::~BlitHelper() { device.destroyPipelineLayout(compute_pipeline_layout); device.destroyPipelineLayout(compute_buffer_pipeline_layout); device.destroyPipelineLayout(two_textures_pipeline_layout); + device.destroyPipelineLayout(single_texture_pipeline_layout); + device.destroyPipelineLayout(three_textures_pipeline_layout); device.destroyShaderModule(full_screen_vert); device.destroyShaderModule(d24s8_to_rgba8_comp); device.destroyShaderModule(depth_to_buffer_comp); device.destroyShaderModule(blit_depth_stencil_frag); + // Destroy texture filtering shader modules + device.destroyShaderModule(bicubic_frag); + device.destroyShaderModule(scale_force_frag); + device.destroyShaderModule(xbrz_frag); + device.destroyShaderModule(mmpx_frag); + device.destroyShaderModule(refine_frag); device.destroyPipeline(depth_to_buffer_pipeline); device.destroyPipeline(d24s8_to_rgba8_pipeline); device.destroyPipeline(depth_blit_pipeline); + // Destroy texture filtering pipelines + device.destroyPipeline(bicubic_pipeline); + device.destroyPipeline(scale_force_pipeline); + device.destroyPipeline(xbrz_pipeline); + device.destroyPipeline(mmpx_pipeline); + device.destroyPipeline(refine_pipeline); device.destroySampler(linear_sampler); device.destroySampler(nearest_sampler); } void BindBlitState(vk::CommandBuffer cmdbuf, vk::PipelineLayout layout, - const VideoCore::TextureBlit& blit) { + const VideoCore::TextureBlit& blit, const Surface& dest) { const vk::Offset2D offset{ .x = std::min(blit.dst_rect.left, blit.dst_rect.right), .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), @@ -272,8 +352,9 @@ void BindBlitState(vk::CommandBuffer cmdbuf, vk::PipelineLayout layout, }; cmdbuf.setViewport(0, viewport); cmdbuf.setScissor(0, scissor); - cmdbuf.pushConstants(layout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(push_constants), - &push_constants); + cmdbuf.pushConstants(layout, + vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 0, + sizeof(push_constants), &push_constants); } bool BlitHelper::BlitDepthStencil(Surface& source, Surface& dest, @@ -300,12 +381,12 @@ bool BlitHelper::BlitDepthStencil(Surface& source, Surface& dest, }; renderpass_cache.BeginRendering(depth_pass); - scheduler.Record([blit, descriptor_set, this](vk::CommandBuffer cmdbuf) { + scheduler.Record([blit, descriptor_set, &dest, this](vk::CommandBuffer cmdbuf) { const vk::PipelineLayout layout = two_textures_pipeline_layout; cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, depth_blit_pipeline); cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, descriptor_set, {}); - BindBlitState(cmdbuf, layout, blit); + BindBlitState(cmdbuf, layout, blit, dest); cmdbuf.draw(3, 1, 0, 0); }); scheduler.MakeDirty(StateFlags::Pipeline); @@ -531,7 +612,7 @@ vk::Pipeline BlitHelper::MakeDepthStencilBlitPipeline() { .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO, .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, .pDepthStencilState = &PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, - .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_EMPTY_CREATE_INFO, + .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO, .layout = two_textures_pipeline_layout, .renderPass = renderpass, @@ -547,4 +628,258 @@ vk::Pipeline BlitHelper::MakeDepthStencilBlitPipeline() { return VK_NULL_HANDLE; } +bool BlitHelper::Filter(Surface& surface, const VideoCore::TextureBlit& blit) { + const auto filter = Settings::values.texture_filter.GetValue(); + const bool is_depth = + surface.type == VideoCore::SurfaceType::Depth || + surface.type == VideoCore::SurfaceType::DepthStencil; // Skip filtering for depth textures + // and when no filter is selected + if (filter == Settings::TextureFilter::NoFilter || is_depth) { + return false; + } // Only filter base mipmap level + if (blit.src_level != 0) { + return true; + } + + switch (filter) { + case TextureFilter::Anime4K: + FilterAnime4K(surface, blit); + break; + case TextureFilter::Bicubic: + FilterBicubic(surface, blit); + break; + case TextureFilter::ScaleForce: + FilterScaleForce(surface, blit); + break; + case TextureFilter::xBRZ: + FilterXbrz(surface, blit); + break; + case TextureFilter::MMPX: + FilterMMPX(surface, blit); + break; + default: + LOG_ERROR(Render_Vulkan, "Unknown texture filter {}", filter); + return false; + } + return true; +} + +void BlitHelper::FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit) { + FilterPassThreeTextures(surface, surface, surface, surface, refine_pipeline, + three_textures_pipeline_layout, blit); +} + +void BlitHelper::FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit) { + FilterPass(surface, surface, bicubic_pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterScaleForce(Surface& surface, const VideoCore::TextureBlit& blit) { + FilterPass(surface, surface, scale_force_pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit) { + FilterPass(surface, surface, xbrz_pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterMMPX(Surface& surface, const VideoCore::TextureBlit& blit) { + FilterPass(surface, surface, mmpx_pipeline, single_texture_pipeline_layout, blit); +} + +vk::Pipeline BlitHelper::MakeFilterPipeline(vk::ShaderModule fragment_shader, + vk::PipelineLayout layout) { + const std::array stages = MakeStages(full_screen_vert, fragment_shader); + // Use color format for render pass, always a color target + const auto renderpass = renderpass_cache.GetRenderpass(VideoCore::PixelFormat::RGBA8, + VideoCore::PixelFormat::Invalid, false); + + vk::GraphicsPipelineCreateInfo pipeline_info = { + .stageCount = static_cast(stages.size()), + .pStages = stages.data(), + .pVertexInputState = &PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .pInputAssemblyState = &PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .pTessellationState = nullptr, + .pViewportState = &PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .pDepthStencilState = nullptr, + .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .layout = layout, + .renderPass = renderpass, + }; + + if (const auto result = device.createGraphicsPipeline({}, pipeline_info); + result.result == vk::Result::eSuccess) { + return result.value; + } else { + LOG_CRITICAL(Render_Vulkan, "Filter pipeline creation failed!"); + UNREACHABLE(); + } +} + +void BlitHelper::FilterPass(Surface& source, Surface& dest, vk::Pipeline pipeline, + vk::PipelineLayout layout, const VideoCore::TextureBlit& blit) { + const auto texture_descriptor_set = single_texture_provider.Commit(); + update_queue.AddImageSampler(texture_descriptor_set, 0, 0, source.ImageView(0), linear_sampler, + vk::ImageLayout::eGeneral); + + const bool is_depth = dest.type == VideoCore::SurfaceType::Depth || + dest.type == VideoCore::SurfaceType::DepthStencil; + const auto color_format = is_depth ? VideoCore::PixelFormat::Invalid : dest.pixel_format; + const auto depth_format = is_depth ? dest.pixel_format : VideoCore::PixelFormat::Invalid; + const auto renderpass = renderpass_cache.GetRenderpass(color_format, depth_format, false); + + const RenderPass render_pass = { + .framebuffer = dest.Framebuffer(), + .render_pass = renderpass, + .render_area = + { + .offset = {0, 0}, + .extent = {dest.GetScaledWidth(), dest.GetScaledHeight()}, + }, + }; + renderpass_cache.BeginRendering(render_pass); + const float src_scale = static_cast(source.GetResScale()); + // Calculate normalized texture coordinates like OpenGL does + const auto src_extent = source.RealExtent(false); // Get unscaled texture extent + const float tex_scale_x = + static_cast(blit.src_rect.GetWidth()) / static_cast(src_extent.width); + const float tex_scale_y = + static_cast(blit.src_rect.GetHeight()) / static_cast(src_extent.height); + const float tex_offset_x = + static_cast(blit.src_rect.left) / static_cast(src_extent.width); + const float tex_offset_y = + static_cast(blit.src_rect.bottom) / static_cast(src_extent.height); + + scheduler.Record([pipeline, layout, texture_descriptor_set, blit, tex_scale_x, tex_scale_y, + tex_offset_x, tex_offset_y, src_scale](vk::CommandBuffer cmdbuf) { + const FilterPushConstants push_constants{.tex_scale = {tex_scale_x, tex_scale_y}, + .tex_offset = {tex_offset_x, tex_offset_y}, + .res_scale = src_scale}; + + cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + + // Bind single texture descriptor set + cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, + texture_descriptor_set, {}); + + cmdbuf.pushConstants(layout, FILTER_PUSH_CONSTANT_RANGE.stageFlags, + FILTER_PUSH_CONSTANT_RANGE.offset, FILTER_PUSH_CONSTANT_RANGE.size, + &push_constants); + + // Set up viewport and scissor for filtering (don't use BindBlitState as it overwrites push + // constants) + const vk::Offset2D offset{ + .x = std::min(blit.dst_rect.left, blit.dst_rect.right), + .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), + }; + const vk::Extent2D extent{ + .width = blit.dst_rect.GetWidth(), + .height = blit.dst_rect.GetHeight(), + }; + const vk::Viewport viewport{ + .x = static_cast(offset.x), + .y = static_cast(offset.y), + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + const vk::Rect2D scissor{ + .offset = offset, + .extent = extent, + }; + cmdbuf.setViewport(0, viewport); + cmdbuf.setScissor(0, scissor); + cmdbuf.draw(3, 1, 0, 0); + }); + scheduler.MakeDirty(StateFlags::Pipeline); +} + +void BlitHelper::FilterPassThreeTextures(Surface& source1, Surface& source2, Surface& source3, + Surface& dest, vk::Pipeline pipeline, + vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit) { + const auto texture_descriptor_set = three_textures_provider.Commit(); + + update_queue.AddImageSampler(texture_descriptor_set, 0, 0, source1.ImageView(0), linear_sampler, + vk::ImageLayout::eGeneral); + update_queue.AddImageSampler(texture_descriptor_set, 1, 0, source2.ImageView(0), linear_sampler, + vk::ImageLayout::eGeneral); + update_queue.AddImageSampler(texture_descriptor_set, 2, 0, source3.ImageView(0), linear_sampler, + vk::ImageLayout::eGeneral); + + const bool is_depth = dest.type == VideoCore::SurfaceType::Depth || + dest.type == VideoCore::SurfaceType::DepthStencil; + const auto color_format = is_depth ? VideoCore::PixelFormat::Invalid : dest.pixel_format; + const auto depth_format = is_depth ? dest.pixel_format : VideoCore::PixelFormat::Invalid; + const auto renderpass = renderpass_cache.GetRenderpass(color_format, depth_format, false); + + const RenderPass render_pass = { + .framebuffer = dest.Framebuffer(), + .render_pass = renderpass, + .render_area = + { + .offset = {0, 0}, + .extent = {dest.GetScaledWidth(), dest.GetScaledHeight()}, + }, + }; + renderpass_cache.BeginRendering(render_pass); + + const float src_scale = static_cast(source1.GetResScale()); + // Calculate normalized texture coordinates like OpenGL does + const auto src_extent = source1.RealExtent(false); // Get unscaled texture extent + const float tex_scale_x = + static_cast(blit.src_rect.GetWidth()) / static_cast(src_extent.width); + const float tex_scale_y = + static_cast(blit.src_rect.GetHeight()) / static_cast(src_extent.height); + const float tex_offset_x = + static_cast(blit.src_rect.left) / static_cast(src_extent.width); + const float tex_offset_y = + static_cast(blit.src_rect.bottom) / static_cast(src_extent.height); + + scheduler.Record([pipeline, layout, texture_descriptor_set, blit, tex_scale_x, tex_scale_y, + tex_offset_x, tex_offset_y, src_scale](vk::CommandBuffer cmdbuf) { + const FilterPushConstants push_constants{.tex_scale = {tex_scale_x, tex_scale_y}, + .tex_offset = {tex_offset_x, tex_offset_y}, + .res_scale = src_scale}; + + cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + + // Bind single texture descriptor set + cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, + texture_descriptor_set, {}); + + cmdbuf.pushConstants(layout, FILTER_PUSH_CONSTANT_RANGE.stageFlags, + FILTER_PUSH_CONSTANT_RANGE.offset, FILTER_PUSH_CONSTANT_RANGE.size, + &push_constants); + + // Set up viewport and scissor using safe viewport like working filters + const vk::Offset2D offset{ + .x = std::min(blit.dst_rect.left, blit.dst_rect.right), + .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), + }; + const vk::Extent2D extent{ + .width = blit.dst_rect.GetWidth(), + .height = blit.dst_rect.GetHeight(), + }; + const vk::Viewport viewport{ + .x = static_cast(offset.x), + .y = static_cast(offset.y), + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + const vk::Rect2D scissor{ + .offset = offset, + .extent = extent, + }; + cmdbuf.setViewport(0, viewport); + cmdbuf.setScissor(0, scissor); + cmdbuf.draw(3, 1, 0, 0); + }); + scheduler.MakeDirty(StateFlags::Pipeline); +} + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.h b/src/video_core/renderer_vulkan/vk_blit_helper.h index d9b5c7760..030c65f5c 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.h +++ b/src/video_core/renderer_vulkan/vk_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. @@ -27,6 +27,7 @@ public: explicit BlitHelper(const Instance& instance, Scheduler& scheduler, RenderManager& renderpass_cache, DescriptorUpdateQueue& update_queue); ~BlitHelper(); + bool Filter(Surface& surface, const VideoCore::TextureBlit& blit); bool BlitDepthStencil(Surface& source, Surface& dest, const VideoCore::TextureBlit& blit); @@ -38,6 +39,23 @@ public: private: vk::Pipeline MakeComputePipeline(vk::ShaderModule shader, vk::PipelineLayout layout); vk::Pipeline MakeDepthStencilBlitPipeline(); + vk::Pipeline MakeFilterPipeline(vk::ShaderModule fragment_shader, vk::PipelineLayout layout); + + void FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterScaleForce(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterMMPX(Surface& surface, const VideoCore::TextureBlit& blit); + + void FilterPass(Surface& source, Surface& dest, vk::Pipeline pipeline, + vk::PipelineLayout layout, const VideoCore::TextureBlit& blit); + + void FilterPassThreeTextures(Surface& source1, Surface& source2, Surface& source3, + Surface& dest, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit); + + void FilterPassYGradient(Surface& source, Surface& dest, vk::Pipeline pipeline, + vk::PipelineLayout layout, const VideoCore::TextureBlit& blit); private: const Instance& instance; @@ -51,18 +69,32 @@ private: DescriptorHeap compute_provider; DescriptorHeap compute_buffer_provider; DescriptorHeap two_textures_provider; + DescriptorHeap single_texture_provider; + DescriptorHeap three_textures_provider; vk::PipelineLayout compute_pipeline_layout; vk::PipelineLayout compute_buffer_pipeline_layout; vk::PipelineLayout two_textures_pipeline_layout; + vk::PipelineLayout single_texture_pipeline_layout; + vk::PipelineLayout three_textures_pipeline_layout; vk::ShaderModule full_screen_vert; vk::ShaderModule d24s8_to_rgba8_comp; vk::ShaderModule depth_to_buffer_comp; vk::ShaderModule blit_depth_stencil_frag; + vk::ShaderModule bicubic_frag; + vk::ShaderModule scale_force_frag; + vk::ShaderModule xbrz_frag; + vk::ShaderModule mmpx_frag; + vk::ShaderModule refine_frag; vk::Pipeline d24s8_to_rgba8_pipeline; vk::Pipeline depth_to_buffer_pipeline; vk::Pipeline depth_blit_pipeline; + vk::Pipeline bicubic_pipeline; + vk::Pipeline scale_force_pipeline; + vk::Pipeline xbrz_pipeline; + vk::Pipeline mmpx_pipeline; + vk::Pipeline refine_pipeline; vk::Sampler linear_sampler; vk::Sampler nearest_sampler; }; diff --git a/src/video_core/renderer_vulkan/vk_resource_pool.cpp b/src/video_core/renderer_vulkan/vk_resource_pool.cpp index 0021167e4..28ef0504c 100644 --- a/src/video_core/renderer_vulkan/vk_resource_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_resource_pool.cpp @@ -1,4 +1,4 @@ -// Copyright 2020 yuzu Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -103,13 +103,14 @@ vk::CommandBuffer CommandPool::Commit() { return cmd_buffers[index]; } -constexpr u32 DESCRIPTOR_SET_BATCH = 32; +constexpr u32 DESCRIPTOR_SET_BATCH = 64; +constexpr u32 DESCRIPTOR_MULTIPLIER = 4; // Increase capacity of each pool DescriptorHeap::DescriptorHeap(const Instance& instance, MasterSemaphore* master_semaphore, std::span bindings, u32 descriptor_heap_count_) : ResourcePool{master_semaphore, DESCRIPTOR_SET_BATCH}, device{instance.GetDevice()}, - descriptor_heap_count{descriptor_heap_count_} { + descriptor_heap_count{descriptor_heap_count_ * DESCRIPTOR_MULTIPLIER} { // Increase pool size // Create descriptor set layout. const vk::DescriptorSetLayoutCreateInfo layout_ci = { .bindingCount = static_cast(bindings.size()), diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index fdaa4ef2e..34ce793ad 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1,9 +1,23 @@ -// 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. +#include "video_core/renderer_vulkan/vk_texture_runtime.h" + +#include +#include +#include +#include #include #include +#include +#include "video_core/custom_textures/custom_tex_manager.h" +#include "video_core/rasterizer_cache/pixel_format.h" +#include "video_core/rasterizer_cache/surface_params.h" +#include "video_core/renderer_base.h" +#include "video_core/renderer_vulkan/vk_blit_helper.h" +#include "video_core/renderer_vulkan/vk_descriptor_update_queue.h" +#include "video_core/renderer_vulkan/vk_stream_buffer.h" #include "common/literals.h" #include "common/microprofile.h" @@ -451,6 +465,49 @@ void TextureRuntime::ClearTextureWithRenderpass(Surface& surface, }); } +vk::UniqueImageView MakeFramebufferImageView(vk::Device device, vk::Image image, vk::Format format, + vk::ImageAspectFlags aspect, u32 base_level = 0) { + // For framebuffer attachments, we must always use levelCount=1 to avoid + // Vulkan validation errors about mipLevel being outside of the allowed range + const vk::ImageViewCreateInfo view_info = { + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange{ + .aspectMask = aspect, + .baseMipLevel = base_level, // Use the specified base mip level + .levelCount = 1, // Framebuffers require a single mip level + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + return device.createImageViewUnique(view_info); +} + +vk::ImageView CreateFramebufferImageView(const Instance* instance, vk::Image image, + vk::Format format, vk::ImageAspectFlags aspect) { + // Always create a view with a single mip level for framebuffer attachments + const vk::ImageViewCreateInfo view_info = { + .image = image, + .viewType = vk::ImageViewType::e2D, + .format = format, + .subresourceRange{ + .aspectMask = aspect, + .baseMipLevel = 0, + .levelCount = 1, // Always use 1 for framebuffers to avoid Vulkan validation errors + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + return instance->GetDevice().createImageView(view_info); +} + +bool IsImagelessFramebufferSupported(const Instance* instance) { + // We're not using imageless framebuffers to avoid validation errors + // Even if the extension is supported, we'll use standard framebuffers for better compatibility + return false; +} + bool TextureRuntime::CopyTextures(Surface& source, Surface& dest, std::span copies) { renderpass_cache.EndRendering(); @@ -700,7 +757,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param : SurfaceBase{params}, runtime{&runtime_}, instance{&runtime_.GetInstance()}, scheduler{&runtime_.GetScheduler()}, traits{instance->GetTraits(pixel_format)} { - if (pixel_format == VideoCore::PixelFormat::Invalid) { + if (pixel_format == VideoCore::PixelFormat::Invalid || !traits.transfer_support) { return; } @@ -720,18 +777,25 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param flags |= vk::ImageCreateFlagBits::eMutableFormat; } + // Ensure color formats have the color attachment bit set for framebuffers + auto usage = traits.usage; + const bool is_color = + (traits.aspect & vk::ImageAspectFlagBits::eColor) != vk::ImageAspectFlags{}; + if (is_color) { + usage |= vk::ImageUsageFlagBits::eColorAttachment; + } + const bool need_format_list = is_mutable && instance->IsImageFormatListSupported(); - handles[0] = MakeHandle(instance, width, height, levels, texture_type, format, traits.usage, - flags, traits.aspect, need_format_list, DebugName(false)); + handles[0] = MakeHandle(instance, width, height, levels, texture_type, format, usage, flags, + traits.aspect, need_format_list, DebugName(false)); raw_images.emplace_back(handles[0].image); if (res_scale != 1) { handles[1] = MakeHandle(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, format, - traits.usage, flags, traits.aspect, need_format_list, DebugName(true)); + usage, flags, traits.aspect, need_format_list, DebugName(true)); raw_images.emplace_back(handles[1].image); } - runtime->renderpass_cache.EndRendering(); scheduler->Record([raw_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { const auto barriers = MakeInitBarriers(aspect, raw_images); @@ -788,6 +852,50 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface material = mat; } +Surface::Surface(TextureRuntime& runtime_, u32 width_, u32 height_, VideoCore::PixelFormat format_) + : SurfaceBase{{ + .width = width_, + .height = height_, + .pixel_format = format_, + .type = VideoCore::SurfaceType::Texture, + }}, + runtime{&runtime_}, instance{&runtime_.GetInstance()}, scheduler{&runtime_.GetScheduler()}, + traits{instance->GetTraits(format_)} { + + // Create texture with requested size and format + const vk::ImageUsageFlags usage = + vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled; + + handles[0] = MakeHandle(instance, width_, height_, 1, VideoCore::TextureType::Texture2D, + traits.native, usage, {}, traits.aspect, false, "Temporary Surface"); + + // Create image view + const vk::ImageViewCreateInfo view_info = { + .image = handles[0].image, + .viewType = vk::ImageViewType::e2D, + .format = traits.native, + .subresourceRange{ + .aspectMask = traits.aspect, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + + handles[0].image_view = instance->GetDevice().createImageViewUnique(view_info); + + runtime->renderpass_cache.EndRendering(); + scheduler->Record( + [raw_images = std::array{Image()}, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + const auto barriers = MakeInitBarriers(aspect, raw_images); + cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, {}, {}, barriers); + }); +} + Surface::~Surface() { if (!handles[0].image_view) { return; @@ -876,14 +984,23 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, runtime->upload_buffer.Commit(staging.size); if (res_scale != 1) { - const VideoCore::TextureBlit blit = { - .src_level = upload.texture_level, - .dst_level = upload.texture_level, - .src_rect = upload.texture_rect, - .dst_rect = upload.texture_rect * res_scale, - }; - - BlitScale(blit, true); + // Always ensure the scaled image exists + if (!handles[1].image) { + // This will create handles[1] and perform the initial scaling + ScaleUp(res_scale); + } else { + // Update the scaled version of the uploaded area + const VideoCore::TextureBlit blit = { + .src_level = upload.texture_level, + .dst_level = upload.texture_level, + .src_rect = upload.texture_rect, + .dst_rect = upload.texture_rect * res_scale, + }; + // Only apply texture filtering when upscaling, matching OpenGL behavior + if (res_scale != 1 && !runtime->blit_helper.Filter(*this, blit)) { + BlitScale(blit, true); + } + } } } @@ -1251,7 +1368,18 @@ vk::ImageView Surface::ImageView(u32 index) const noexcept { vk::ImageView Surface::FramebufferView() noexcept { is_framebuffer = true; - return ImageView(); + + // If we already have a framebuffer-compatible view, return it + if (framebuffer_view) { + return framebuffer_view.get(); + } + + // Create a new view with a single mip level for framebuffer compatibility + // This is critical to avoid VUID-VkFramebufferCreateInfo-pAttachments-00883 validation errors + framebuffer_view = MakeFramebufferImageView( + instance->GetDevice(), Image(), instance->GetTraits(pixel_format).native, Aspect(), 0); + + return framebuffer_view.get(); } vk::ImageView Surface::DepthView() noexcept { @@ -1329,6 +1457,8 @@ vk::ImageView Surface::StorageView() noexcept { } vk::Framebuffer Surface::Framebuffer() noexcept { + is_framebuffer = true; + const u32 index = res_scale == 1 ? 0u : 1u; if (framebuffers[index]) { return framebuffers[index].get(); @@ -1339,7 +1469,8 @@ vk::Framebuffer Surface::Framebuffer() noexcept { const auto depth_format = is_depth ? pixel_format : PixelFormat::Invalid; const auto render_pass = runtime->renderpass_cache.GetRenderpass(color_format, depth_format, false); - const auto attachments = std::array{ImageView()}; + // Use FramebufferView() instead of ImageView() to ensure single mip level + const auto attachments = std::array{FramebufferView()}; framebuffers[index] = MakeFramebuffer(instance->GetDevice(), render_pass, GetScaledWidth(), GetScaledHeight(), attachments); return framebuffers[index].get(); @@ -1351,11 +1482,20 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { if (is_depth_stencil && !depth_traits.blit_support) { LOG_WARNING(Render_Vulkan, "Depth scale unsupported by hardware"); return; - } + } // Check if texture filtering is enabled + const bool texture_filter_enabled = + Settings::values.texture_filter.GetValue() != Settings::TextureFilter::NoFilter; - scheduler->Record([src_image = Image(!up_scale), aspect = Aspect(), - filter = MakeFilter(pixel_format), dst_image = Image(up_scale), - blit](vk::CommandBuffer render_cmdbuf) { + // Always use consistent source and destination images for proper scaling + // When upscaling: source = unscaled (0), destination = scaled (1) + // When downscaling: source = scaled (1), destination = unscaled (0) + const vk::Image src_image = up_scale ? Image(0) : Image(1); + const vk::Image dst_image = up_scale ? Image(1) : Image(0); + + scheduler->Record([src_image, aspect = Aspect(), filter = MakeFilter(pixel_format), dst_image, + src_access = AccessFlags(), dst_access = AccessFlags(), blit, + texture_filter_enabled](vk::CommandBuffer render_cmdbuf) { + // Adjust blitting parameters for filtered upscaling const std::array source_offsets = { vk::Offset3D{static_cast(blit.src_rect.left), static_cast(blit.src_rect.bottom), 0}, @@ -1368,7 +1508,15 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { static_cast(blit.dst_rect.bottom), 0}, vk::Offset3D{static_cast(blit.dst_rect.right), static_cast(blit.dst_rect.top), 1}, - }; + }; // Ensure we're using the right filter for texture filtered upscaling + vk::Filter actual_filter; + if (texture_filter_enabled) { + // When texture filtering is enabled, always use LINEAR filtering + actual_filter = vk::Filter::eLinear; + } else { + // When texture filtering is disabled, use the filter appropriate for the texture format + actual_filter = filter; + } const vk::ImageBlit blit_area = { .srcSubresource{ @@ -1389,7 +1537,7 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { const std::array read_barriers = { vk::ImageMemoryBarrier{ - .srcAccessMask = vk::AccessFlagBits::eMemoryWrite, + .srcAccessMask = src_access, .dstAccessMask = vk::AccessFlagBits::eTransferRead, .oldLayout = vk::ImageLayout::eGeneral, .newLayout = vk::ImageLayout::eTransferSrcOptimal, @@ -1399,10 +1547,7 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { .subresourceRange = MakeSubresourceRange(aspect, blit.src_level), }, vk::ImageMemoryBarrier{ - .srcAccessMask = vk::AccessFlagBits::eShaderRead | - vk::AccessFlagBits::eDepthStencilAttachmentRead | - vk::AccessFlagBits::eColorAttachmentRead | - vk::AccessFlagBits::eTransferRead, + .srcAccessMask = dst_access, .dstAccessMask = vk::AccessFlagBits::eTransferWrite, .oldLayout = vk::ImageLayout::eGeneral, .newLayout = vk::ImageLayout::eTransferDstOptimal, @@ -1440,7 +1585,7 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { vk::DependencyFlagBits::eByRegion, {}, {}, read_barriers); render_cmdbuf.blitImage(src_image, vk::ImageLayout::eTransferSrcOptimal, dst_image, - vk::ImageLayout::eTransferDstOptimal, blit_area, filter); + vk::ImageLayout::eTransferDstOptimal, blit_area, actual_filter); render_cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eAllCommands, @@ -1449,9 +1594,9 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { } Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferParams& params, - Surface* color, Surface* depth) + Surface* color, Surface* depth_stencil) : VideoCore::FramebufferParams{params}, - res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)} { + res_scale{color ? color->res_scale : (depth_stencil ? depth_stencil->res_scale : 1u)} { auto& renderpass_cache = runtime.GetRenderpassCache(); if (shadow_rendering && !color) { return; @@ -1468,30 +1613,62 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa } images[index] = surface->Image(); aspects[index] = surface->Aspect(); - image_views[index] = shadow_rendering ? surface->StorageView() : surface->FramebufferView(); }; - boost::container::static_vector attachments; - + // Prepare the surfaces for use in framebuffer if (color) { prepare(0, color); - attachments.emplace_back(image_views[0]); } - if (depth) { - prepare(1, depth); - attachments.emplace_back(image_views[1]); + if (depth_stencil) { + prepare(1, depth_stencil); } const vk::Device device = runtime.GetInstance().GetDevice(); + + // Create appropriate image views for the framebuffer + boost::container::static_vector fb_attachments; + + if (color) { + vk::UniqueImageView single_level_view = + MakeFramebufferImageView(device, color->Image(), color->traits.native, color->Aspect()); + fb_attachments.push_back(single_level_view.get()); + framebuffer_views.push_back(std::move(single_level_view)); + } + + if (depth_stencil) { + vk::UniqueImageView single_level_view = MakeFramebufferImageView( + device, depth_stencil->Image(), depth_stencil->traits.native, depth_stencil->Aspect()); + fb_attachments.push_back(single_level_view.get()); + framebuffer_views.push_back(std::move(single_level_view)); + } if (shadow_rendering) { - render_pass = - renderpass_cache.GetRenderpass(PixelFormat::Invalid, PixelFormat::Invalid, false); - framebuffer = MakeFramebuffer(device, render_pass, color->GetScaledWidth(), - color->GetScaledHeight(), {}); + // For shadow rendering, we need a special render pass with depth-only + // Since shadow rendering doesn't output to color buffer, we use depth-only render pass + render_pass = renderpass_cache.GetRenderpass(PixelFormat::Invalid, formats[1], false); + + // Find the depth attachment in fb_attachments + boost::container::static_vector shadow_attachments; + if (depth_stencil) { + // Depth attachment is the last one added (after color if present) + shadow_attachments.push_back(fb_attachments.back()); + } else if (!fb_attachments.empty()) { + // Fallback to first attachment if no depth_stencil + shadow_attachments.push_back(fb_attachments[0]); + } + + // Create framebuffer with depth attachment only + framebuffer = MakeFramebuffer( + device, render_pass, color ? color->GetScaledWidth() : depth_stencil->GetScaledWidth(), + color ? color->GetScaledHeight() : depth_stencil->GetScaledHeight(), + shadow_attachments); } else { - render_pass = renderpass_cache.GetRenderpass(formats[0], formats[1], false); - framebuffer = MakeFramebuffer(device, render_pass, width, height, attachments); + // For normal rendering, create a render pass that matches our attachments + render_pass = renderpass_cache.GetRenderpass( + color ? formats[0] : PixelFormat::Invalid, + depth_stencil ? formats[1] : PixelFormat::Invalid, false); + // Create the framebuffer with attachments matching the render pass + framebuffer = MakeFramebuffer(device, render_pass, width, height, fb_attachments); } } diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index e1745b22b..eb48451ed 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.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. @@ -110,6 +110,8 @@ public: explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceParams& params); explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceBase& surface, const VideoCore::Material* materal); + explicit Surface(TextureRuntime& runtime, u32 width_, u32 height_, + VideoCore::PixelFormat format_); ~Surface(); Surface(const Surface&) = delete; @@ -128,6 +130,21 @@ public: /// Returns the image view at index, otherwise the base view vk::ImageView ImageView(u32 index = 1) const noexcept; + /// Returns width of the surface + u32 GetWidth() const noexcept { + return width; + } + + /// Returns height of the surface + u32 GetHeight() const noexcept { + return height; + } + + /// Returns resolution scale of the surface + u32 GetResScale() const noexcept { + return res_scale; + } + /// Returns a copy of the upscaled image handle, used for feedback loops. vk::ImageView CopyImageView() noexcept; @@ -184,6 +201,7 @@ public: std::array handles{}; std::array framebuffers{}; Handle copy_handle; + vk::UniqueImageView framebuffer_view; vk::UniqueImageView depth_view; vk::UniqueImageView stencil_view; vk::UniqueImageView storage_view; @@ -244,6 +262,7 @@ private: std::array image_views{}; vk::UniqueFramebuffer framebuffer; vk::RenderPass render_pass; + std::vector framebuffer_views; std::array aspects{}; std::array formats{VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid};