Alphas on lsfg

This commit is contained in:
CamilleLaVey
2026-08-12 23:24:57 -04:00
parent 4f4f4a3586
commit 61f3a1d135
8 changed files with 559 additions and 6 deletions
+4
View File
@@ -127,6 +127,10 @@ add_library(video_core STATIC
renderer_vulkan/present/filters.h
renderer_vulkan/present/frame_gen.cpp
renderer_vulkan/present/frame_gen.h
renderer_vulkan/present/lsfg_alpha.cpp
renderer_vulkan/present/lsfg_alpha.h
renderer_vulkan/present/lsfg_common.cpp
renderer_vulkan/present/lsfg_common.h
renderer_vulkan/present/lsfg_mipmaps.cpp
renderer_vulkan/present/lsfg_mipmaps.h
renderer_vulkan/present/lsfg_shaders.cpp
@@ -63,7 +63,7 @@ void FrameGen::Process(const Device& device, Frame* frame) {
Rebuild(device, extent);
}
mipmaps->Dispatch(device, scheduler, *frame->image_view, frame_count);
mipmaps->Dispatch(device, scheduler, *frame->image, *frame->image_view, frame_count);
++frame_count;
const bool dump_requested = Settings::values.frame_gen_dump_flow.GetValue();
@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <vector>
#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_vulkan/present/lsfg_alpha.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr u32 DISPATCH_TILE_SHIFT = 3;
[[nodiscard]] u32 GroupCount(u32 size) {
return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT;
}
[[nodiscard]] VkExtent2D HalveExtent(VkExtent2D extent) {
return VkExtent2D{
.width = (extent.width + 1) >> 1,
.height = (extent.height + 1) >> 1,
};
}
} // Anonymous namespace
LsfgAlpha::LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator,
const LsfgShaders& shaders, vk::DescriptorPool& descriptor_pool,
vk::Sampler& sampler, const LsfgImage& input_)
: input{&input_} {
using namespace VideoCore::FrameGen::PerformanceShader;
passes[0] = LsfgPass(device, shaders, ALPHA[0],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
passes[1] = LsfgPass(device, shaders, ALPHA[1],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
passes[2] = LsfgPass(device, shaders, ALPHA[2],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
passes[3] = LsfgPass(device, shaders, ALPHA[3],
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
{2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
const VkExtent2D half_extent = HalveExtent(input->Extent());
const VkExtent2D quarter_extent = HalveExtent(half_extent);
temp1 = LsfgImage(device, memory_allocator, half_extent);
temp2 = LsfgImage(device, memory_allocator, half_extent);
for (size_t i = 0; i < LSFG_ALPHA_OUTPUTS; ++i) {
temp3[i] = LsfgImage(device, memory_allocator, quarter_extent);
for (size_t j = 0; j < LSFG_ALPHA_HISTORY; ++j) {
out_images[j][i] = LsfgImage(device, memory_allocator, quarter_extent);
}
}
std::vector<VkDescriptorSetLayout> layouts;
for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) {
layouts.push_back(passes[i].SetLayout());
}
for (size_t i = 0; i < LSFG_ALPHA_HISTORY; ++i) {
layouts.push_back(passes[3].SetLayout());
}
owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts);
for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) {
descriptor_sets[i] = owned_sets[i];
}
for (size_t i = 0; i < LSFG_ALPHA_HISTORY; ++i) {
last_descriptor_sets[i] = owned_sets[LSFG_ALPHA_STAGES - 1 + i];
}
LsfgDescriptorWriter(descriptor_sets[0])
.AddSampler(*sampler)
.AddSampledImage(*input)
.AddStorageImage(temp1)
.Build(device);
LsfgDescriptorWriter(descriptor_sets[1])
.AddSampler(*sampler)
.AddSampledImage(temp1)
.AddStorageImage(temp2)
.Build(device);
LsfgDescriptorWriter(descriptor_sets[2])
.AddSampler(*sampler)
.AddSampledImage(temp2)
.AddStorageImages(temp3)
.Build(device);
for (size_t i = 0; i < LSFG_ALPHA_HISTORY; ++i) {
LsfgDescriptorWriter(last_descriptor_sets[i])
.AddSampler(*sampler)
.AddSampledImages(temp3)
.AddStorageImages(out_images[i])
.Build(device);
}
}
void LsfgAlpha::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count) {
const VkExtent2D half_extent = temp1.Extent();
u32 groups_x = GroupCount(half_extent.width);
u32 groups_y = GroupCount(half_extent.height);
LsfgBarriers(cmdbuf).ReadToWrite(temp1).Build();
passes[0].Bind(cmdbuf, descriptor_sets[0]);
cmdbuf.Dispatch(groups_x, groups_y, 1);
LsfgBarriers(cmdbuf).WriteToRead(temp1).ReadToWrite(temp2).Build();
passes[1].Bind(cmdbuf, descriptor_sets[1]);
cmdbuf.Dispatch(groups_x, groups_y, 1);
const VkExtent2D quarter_extent = temp3[0].Extent();
groups_x = GroupCount(quarter_extent.width);
groups_y = GroupCount(quarter_extent.height);
LsfgBarriers(cmdbuf).WriteToRead(temp2).ReadToWriteAll(temp3).Build();
passes[2].Bind(cmdbuf, descriptor_sets[2]);
cmdbuf.Dispatch(groups_x, groups_y, 1);
const size_t slot = frame_count % LSFG_ALPHA_HISTORY;
LsfgBarriers(cmdbuf).WriteToReadAll(temp3).ReadToWriteAll(out_images[slot]).Build();
passes[3].Bind(cmdbuf, last_descriptor_sets[slot]);
cmdbuf.Dispatch(groups_x, groups_y, 1);
}
} // namespace Vulkan
@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <array>
#include "common/common_types.h"
#include "video_core/renderer_vulkan/present/lsfg_common.h"
namespace Vulkan {
class Device;
class LsfgShaders;
class Scheduler;
constexpr size_t LSFG_ALPHA_STAGES = 4;
constexpr size_t LSFG_ALPHA_HISTORY = 3;
constexpr size_t LSFG_ALPHA_OUTPUTS = 2;
using LsfgAlphaOutputs = std::array<LsfgImage, LSFG_ALPHA_OUTPUTS>;
class LsfgAlpha {
public:
LsfgAlpha() = default;
LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders,
vk::DescriptorPool& descriptor_pool, vk::Sampler& sampler, const LsfgImage& input);
void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count);
[[nodiscard]] const LsfgAlphaOutputs& GetOutputs(u64 frame_count) const {
return out_images[frame_count % LSFG_ALPHA_HISTORY];
}
[[nodiscard]] const std::array<LsfgAlphaOutputs, LSFG_ALPHA_HISTORY>& GetAllOutputs() const {
return out_images;
}
private:
const LsfgImage* input{};
std::array<LsfgPass, LSFG_ALPHA_STAGES> passes;
std::array<VkDescriptorSet, LSFG_ALPHA_STAGES - 1> descriptor_sets{};
std::array<VkDescriptorSet, LSFG_ALPHA_HISTORY> last_descriptor_sets{};
vk::DescriptorSets owned_sets;
LsfgImage temp1;
LsfgImage temp2;
std::array<LsfgImage, LSFG_ALPHA_OUTPUTS> temp3;
std::array<LsfgAlphaOutputs, LSFG_ALPHA_HISTORY> out_images;
};
} // namespace Vulkan
@@ -0,0 +1,187 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include "video_core/renderer_vulkan/present/lsfg_common.h"
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
#include "video_core/renderer_vulkan/present/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
namespace Vulkan {
namespace {
constexpr u32 DESCRIPTORS_PER_TYPE = 4096;
VkImageMemoryBarrier MakeBarrier(const LsfgImage& image, VkAccessFlags src_access,
VkAccessFlags dst_access) {
return VkImageMemoryBarrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = src_access,
.dstAccessMask = dst_access,
.oldLayout = image.Layout(),
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image.Handle(),
.subresourceRange{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
}
} // Anonymous namespace
LsfgImage::LsfgImage(const Device& device, MemoryAllocator& memory_allocator, VkExtent2D extent_,
VkFormat format)
: extent{std::max(1u, extent_.width), std::max(1u, extent_.height)} {
image = CreateWrappedImage(memory_allocator, extent, format);
view = CreateWrappedImageView(device, image, format);
}
LsfgBarriers& LsfgBarriers::Push(LsfgImage& image, VkAccessFlags src_access,
VkAccessFlags dst_access) {
barriers.push_back(MakeBarrier(image, src_access, dst_access));
image.SetLayout(VK_IMAGE_LAYOUT_GENERAL);
return *this;
}
LsfgBarriers& LsfgBarriers::WriteToRead(LsfgImage& image) {
return Push(image, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
}
LsfgBarriers& LsfgBarriers::ReadToWrite(LsfgImage& image) {
return Push(image, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT);
}
void LsfgBarriers::Build() {
if (barriers.empty()) {
return;
}
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, barriers);
barriers.clear();
}
LsfgDescriptorWriter& LsfgDescriptorWriter::PushImage(VkDescriptorType type, VkSampler sampler,
VkImageView view) {
image_infos.push_back(VkDescriptorImageInfo{
.sampler = sampler,
.imageView = view,
.imageLayout = view == VK_NULL_HANDLE ? VK_IMAGE_LAYOUT_UNDEFINED
: VK_IMAGE_LAYOUT_GENERAL,
});
writes.push_back(VkWriteDescriptorSet{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = nullptr,
.dstSet = set,
.dstBinding = binding++,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = type,
.pImageInfo = &image_infos.back(),
.pBufferInfo = nullptr,
.pTexelBufferView = nullptr,
});
return *this;
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampler(VkSampler sampler) {
return PushImage(VK_DESCRIPTOR_TYPE_SAMPLER, sampler, VK_NULL_HANDLE);
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampledImage(const LsfgImage& image) {
return PushImage(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_NULL_HANDLE, image.View());
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddStorageImage(const LsfgImage& image) {
return PushImage(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_NULL_HANDLE, image.View());
}
LsfgDescriptorWriter& LsfgDescriptorWriter::AddUniformBuffer(VkBuffer buffer, VkDeviceSize size) {
buffer_infos.push_back(VkDescriptorBufferInfo{
.buffer = buffer,
.offset = 0,
.range = size,
});
writes.push_back(VkWriteDescriptorSet{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = nullptr,
.dstSet = set,
.dstBinding = binding++,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
.pImageInfo = nullptr,
.pBufferInfo = &buffer_infos.back(),
.pTexelBufferView = nullptr,
});
return *this;
}
void LsfgDescriptorWriter::Build(const Device& device) {
if (writes.empty()) {
return;
}
device.GetLogical().UpdateDescriptorSets(writes, {});
writes.clear();
}
LsfgPass::LsfgPass(const Device& device, const LsfgShaders& shaders, u32 shader_id,
LsfgBindings bindings) {
std::vector<VkDescriptorType> types;
for (const auto& [count, type] : bindings) {
types.insert(types.end(), count, type);
}
descriptor_count = static_cast<u32>(types.size());
descriptor_set_layout = CreateWrappedDescriptorSetLayout(
device, std::span<const VkDescriptorType>{types}, VK_SHADER_STAGE_COMPUTE_BIT);
pipeline_layout = CreateWrappedPipelineLayout(device, descriptor_set_layout);
pipeline = CreateWrappedComputePipeline(device, pipeline_layout, shaders.Get(shader_id));
}
void LsfgPass::Bind(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const {
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline_layout, 0, set, {});
}
vk::DescriptorPool CreateLsfgDescriptorPool(const Device& device, u32 max_sets) {
return CreateWrappedDescriptorPool(
device, DESCRIPTORS_PER_TYPE, max_sets,
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_SAMPLER,
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE});
}
vk::Sampler CreateLsfgSampler(const Device& device, VkSamplerAddressMode address_mode,
VkCompareOp compare_op, bool white_border) {
return device.GetLogical().CreateSampler(VkSamplerCreateInfo{
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.magFilter = VK_FILTER_LINEAR,
.minFilter = VK_FILTER_LINEAR,
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
.addressModeU = address_mode,
.addressModeV = address_mode,
.addressModeW = address_mode,
.mipLodBias = 0.0f,
.anisotropyEnable = VK_FALSE,
.maxAnisotropy = 0.0f,
.compareEnable = VK_FALSE,
.compareOp = compare_op,
.minLod = 0.0f,
.maxLod = VK_LOD_CLAMP_NONE,
.borderColor = white_border ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE
: VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
.unnormalizedCoordinates = VK_FALSE,
});
}
} // namespace Vulkan
@@ -0,0 +1,154 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <deque>
#include <initializer_list>
#include <utility>
#include <vector>
#include "common/common_types.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Vulkan {
class Device;
class LsfgShaders;
constexpr VkFormat LSFG_DEFAULT_FORMAT = VK_FORMAT_R8G8B8A8_UNORM;
class LsfgImage {
public:
LsfgImage() = default;
LsfgImage(const Device& device, MemoryAllocator& memory_allocator, VkExtent2D extent_,
VkFormat format = LSFG_DEFAULT_FORMAT);
[[nodiscard]] VkImage Handle() const {
return *image;
}
[[nodiscard]] VkImageView View() const {
return *view;
}
[[nodiscard]] VkExtent2D Extent() const {
return extent;
}
[[nodiscard]] VkImageLayout Layout() const {
return layout;
}
void SetLayout(VkImageLayout new_layout) {
layout = new_layout;
}
private:
vk::Image image;
vk::ImageView view;
VkExtent2D extent{};
VkImageLayout layout{VK_IMAGE_LAYOUT_UNDEFINED};
};
class LsfgBarriers {
public:
explicit LsfgBarriers(vk::CommandBuffer cmdbuf_) : cmdbuf{cmdbuf_} {}
LsfgBarriers& WriteToRead(LsfgImage& image);
LsfgBarriers& ReadToWrite(LsfgImage& image);
template <typename Range>
LsfgBarriers& WriteToReadAll(Range& images) {
for (auto& image : images) {
WriteToRead(image);
}
return *this;
}
template <typename Range>
LsfgBarriers& ReadToWriteAll(Range& images) {
for (auto& image : images) {
ReadToWrite(image);
}
return *this;
}
void Build();
private:
LsfgBarriers& Push(LsfgImage& image, VkAccessFlags src_access, VkAccessFlags dst_access);
vk::CommandBuffer cmdbuf;
std::vector<VkImageMemoryBarrier> barriers;
};
class LsfgDescriptorWriter {
public:
explicit LsfgDescriptorWriter(VkDescriptorSet set_) : set{set_} {}
LsfgDescriptorWriter& AddSampler(VkSampler sampler);
LsfgDescriptorWriter& AddSampledImage(const LsfgImage& image);
LsfgDescriptorWriter& AddStorageImage(const LsfgImage& image);
LsfgDescriptorWriter& AddUniformBuffer(VkBuffer buffer, VkDeviceSize size);
template <typename Range>
LsfgDescriptorWriter& AddSampledImages(const Range& images) {
for (const auto& image : images) {
AddSampledImage(image);
}
return *this;
}
template <typename Range>
LsfgDescriptorWriter& AddStorageImages(const Range& images) {
for (const auto& image : images) {
AddStorageImage(image);
}
return *this;
}
void Build(const Device& device);
private:
LsfgDescriptorWriter& PushImage(VkDescriptorType type, VkSampler sampler, VkImageView view);
VkDescriptorSet set;
u32 binding{};
std::deque<VkDescriptorImageInfo> image_infos;
std::deque<VkDescriptorBufferInfo> buffer_infos;
std::vector<VkWriteDescriptorSet> writes;
};
using LsfgBindings = std::initializer_list<std::pair<u32, VkDescriptorType>>;
class LsfgPass {
public:
LsfgPass() = default;
LsfgPass(const Device& device, const LsfgShaders& shaders, u32 shader_id,
LsfgBindings bindings);
[[nodiscard]] VkDescriptorSetLayout SetLayout() const {
return *descriptor_set_layout;
}
[[nodiscard]] u32 DescriptorCount() const {
return descriptor_count;
}
void Bind(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const;
private:
vk::DescriptorSetLayout descriptor_set_layout;
vk::PipelineLayout pipeline_layout;
vk::Pipeline pipeline;
u32 descriptor_count{};
};
[[nodiscard]] vk::DescriptorPool CreateLsfgDescriptorPool(const Device& device, u32 max_sets);
[[nodiscard]] vk::Sampler CreateLsfgSampler(const Device& device, VkSamplerAddressMode address_mode,
VkCompareOp compare_op, bool white_border);
} // namespace Vulkan
@@ -128,8 +128,8 @@ void LsfgMipmaps::CreateUniformBuffer(f32 flow_scale) {
uniform_buffer.Flush();
}
void LsfgMipmaps::Dispatch(const Device& device, Scheduler& scheduler, VkImageView current_view,
u64 frame_count) {
void LsfgMipmaps::Dispatch(const Device& device, Scheduler& scheduler, VkImage current_image,
VkImageView current_view, u64 frame_count) {
const size_t set_index = frame_count % DESCRIPTOR_SET_COUNT;
const VkDescriptorSet set = descriptor_sets[set_index];
@@ -196,8 +196,30 @@ void LsfgMipmaps::Dispatch(const Device& device, Scheduler& scheduler, VkImageVi
}
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([raw_images, set, groups_x, groups_y, layout = *pipeline_layout,
scheduler.Record([raw_images, current_image, set, groups_x, groups_y,
layout = *pipeline_layout,
compute_pipeline = *pipeline](vk::CommandBuffer cmdbuf) {
const VkImageMemoryBarrier input_barrier{
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = current_image,
.subresourceRange{
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, input_barrier);
for (const VkImage image : raw_images) {
TransitionImageLayout(cmdbuf, image, VK_IMAGE_LAYOUT_GENERAL,
VK_IMAGE_LAYOUT_UNDEFINED);
@@ -22,8 +22,8 @@ public:
explicit LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator,
const LsfgShaders& shaders, VkExtent2D input_extent, f32 flow_scale);
void Dispatch(const Device& device, Scheduler& scheduler, VkImageView current_view,
u64 frame_count);
void Dispatch(const Device& device, Scheduler& scheduler, VkImage current_image,
VkImageView current_view, u64 frame_count);
[[nodiscard]] VkImageView GetLevelView(size_t level) const {
return *image_views[level];