renderer_vulkan: Implement multisample texture runtime

This seems to be enough for simple programs to render with MSAA enabled!
This commit is contained in:
Wunkolo 2026-04-19 09:42:38 -07:00
parent 950803c2b7
commit fdfe63f76c
5 changed files with 239 additions and 70 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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] =

View file

@ -147,6 +147,11 @@ void MakeInitBarriers(vk::ImageAspectFlags aspect, u32 num_images,
}
}
void MakeInitBarriers(vk::ImageAspectFlags aspect, std::span<const vk::Image> images,
std::span<vk::ImageMemoryBarrier> 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<vk::ClearValue, 2> 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<u32>(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<s32>(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<vk::Image, 2> raw_images;
boost::container::static_vector<vk::Image, 3> 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<vk::ImageMemoryBarrier, 3> 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<vk::ImageMemoryBarrier, 4> 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<vk::Image, 3> raw_images;
boost::container::static_vector<vk::Image, 4> 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<vk::ImageMemoryBarrier, 3> 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<vk::ImageMemoryBarrier, 4> 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<vk::ImageMemoryBarrier, 1> 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<vk::ImageView, 2> 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<u32>(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<u32>::max();
u32 num_attachments{};
std::array<vk::ImageView, 2> attachments;
std::array<vk::ImageView, 4> 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 = {

View file

@ -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<vk::Image, 2> Images() const noexcept {
[[nodiscard]] std::array<vk::Image, 4> Images() const noexcept {
return images;
}
[[nodiscard]] std::array<vk::ImageAspectFlags, 2> Aspects() const noexcept {
[[nodiscard]] std::array<vk::ImageAspectFlags, 4> 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<vk::Image, 2> images{};
std::array<vk::ImageView, 2> image_views{};
// Color, Depth, ColorMSAA, DepthMSAA
std::array<vk::Image, 4> images{};
std::array<vk::ImageView, 4> image_views{};
vk::Framebuffer framebuffer{};
vk::RenderPass render_pass{};
std::vector<vk::UniqueImageView> framebuffer_views;
std::array<vk::ImageAspectFlags, 2> aspects{};
std::array<VideoCore::PixelFormat, 2> formats{VideoCore::PixelFormat::Invalid,
VideoCore::PixelFormat::Invalid};
std::array<vk::ImageAspectFlags, 4> aspects{};
std::array<VideoCore::PixelFormat, 4> 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 {