cmd_buffer.cpp

cross platform rendering playground

src/backend/vulkan/cmd_buffer.cpp

21.78 KB
#include <volk.h>

#include <cassert>
#include <cstdlib>
#include <cstring>

#include "backend/vulkan/utils.h"
#include "backend/vulkan/vk_api.h"
#include "backend/vulkan/vk_conversion.h"

namespace rhi {
using namespace vk;

static VkAttachmentLoadOp to_vk_load_op(LoadOp value) {
    switch (value) {
    case LoadOp::LOAD:
        return VK_ATTACHMENT_LOAD_OP_LOAD;
    case LoadOp::CLEAR:
        return VK_ATTACHMENT_LOAD_OP_CLEAR;
    }

    return VK_ATTACHMENT_LOAD_OP_LOAD;
}

static VkAttachmentStoreOp to_vk_store_op(StoreOp value) {
    switch (value) {
    case StoreOp::STORE:
        return VK_ATTACHMENT_STORE_OP_STORE;
    case StoreOp::DISCARD:
        return VK_ATTACHMENT_STORE_OP_DONT_CARE;
    }

    return VK_ATTACHMENT_STORE_OP_STORE;
}

static VkResolveModeFlagBits to_vk_resolve_mode(ResolveOp value) {
    switch (value) {
    case ResolveOp::NONE:
        return VK_RESOLVE_MODE_NONE;
    case ResolveOp::AVERAGE:
        return VK_RESOLVE_MODE_AVERAGE_BIT;
    case ResolveOp::MIN:
        return VK_RESOLVE_MODE_MIN_BIT;
    case ResolveOp::MAX:
        return VK_RESOLVE_MODE_MAX_BIT;
    case ResolveOp::SAMPLE_ZERO:
        return VK_RESOLVE_MODE_SAMPLE_ZERO_BIT;
    }

    return VK_RESOLVE_MODE_NONE;
}

static VkClearValue to_vk_clear_value(const ClearValue &value) {
    VkClearValue result{};
    memcpy(&result, &value, sizeof(VkClearValue));
    return result;
}

static VkRenderingAttachmentInfo to_vk_rendering_attachment(const AttachmentDesc &attachment) {
    VkRenderingAttachmentInfo info{};
    if (attachment.img == nullptr || attachment.img->_handle == nullptr) {
        VEL_CRITICAL("ToVkRenderingAttachment: null or zero-handle attachment image");
        return info;
    }
    info.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;

    info.imageView = attachment.img->_handle;
    auto aspects = attachment.img->desc.aspect;
    const bool depth_read_only =
        attachment.img->desc.image != nullptr && attachment.img->desc.image->state == ResourceState::DepthFetch;
    if (Any(aspects, ImageAspect::Depth) && Any(aspects, ImageAspect::Stencil)) {
        info.imageLayout = depth_read_only ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
                                           : VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
    } else if (Any(aspects, ImageAspect::Depth)) {
        info.imageLayout = depth_read_only ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
                                           : VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
    } else if (Any(aspects, ImageAspect::Stencil)) {
        info.imageLayout = VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL;
    } else {
        info.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    }
    info.loadOp = to_vk_load_op(attachment.loadOp);
    info.storeOp = to_vk_store_op(attachment.storeOp);

    if (attachment.resolveImg) {
        info.resolveMode = to_vk_resolve_mode(attachment.resolveOp);
        info.resolveImageView = attachment.resolveImg->_handle;
        auto resolve_aspects = attachment.resolveImg->desc.aspect;
        if (Any(resolve_aspects, ImageAspect::Depth) && Any(resolve_aspects, ImageAspect::Stencil)) {
            info.resolveImageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
        } else if (Any(resolve_aspects, ImageAspect::Depth)) {
            info.resolveImageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
        } else if (Any(resolve_aspects, ImageAspect::Stencil)) {
            info.resolveImageLayout = VK_IMAGE_LAYOUT_STENCIL_ATTACHMENT_OPTIMAL;
        } else {
            info.resolveImageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
        }
    }

    if (Any(attachment.img->desc.aspect, ImageAspect::Depth) ||
        Any(attachment.img->desc.aspect, ImageAspect::Stencil)) {
        info.clearValue.depthStencil = {
            attachment.clearValue.depthStencil.depth, attachment.clearValue.depthStencil.stencil
        };
    } else {
        info.clearValue.color.float32[0] = attachment.clearValue.color.float32[0];
        info.clearValue.color.float32[1] = attachment.clearValue.color.float32[1];
        info.clearValue.color.float32[2] = attachment.clearValue.color.float32[2];
        info.clearValue.color.float32[3] = attachment.clearValue.color.float32[3];
    }

    return info;
}

bool create_cmd_buffer(Device &device, const CmdBufferDesc &desc, CmdBuffer &cmd) {
    VkCommandBufferAllocateInfo alloc_ci{};
    alloc_ci.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    alloc_ci.commandPool = desc.pool->handle;
    alloc_ci.level = vk::to_vk_cmd_buffer_level(desc.level);
    alloc_ci.commandBufferCount = 1;

    VkCommandBuffer vk_cmd;
    VK_CHECK(vkAllocateCommandBuffers(device.logical, &alloc_ci, &vk_cmd));
    cmd.handle = vk_cmd;
    return true;
}

void destroy_cmd_buffer(Device &device, CmdPool &pool, CmdBuffer &cmd) {
    if (cmd.handle != VK_NULL_HANDLE) {
        VkCommandBuffer vk_cmd = cmd.handle;
        vkFreeCommandBuffers(device.logical, pool.handle, 1, &vk_cmd);
    }

    cmd = {};
}

bool begin_cmd_buffer(CmdBuffer &cmd, const CmdBufferBeginDesc &desc) {
    VkCommandBufferBeginInfo begin_ci{};
    begin_ci.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    begin_ci.flags = vk::to_vk_cmd_buffer_usage_flags(desc.usage);

    VK_CHECK(vkBeginCommandBuffer(cmd.handle, &begin_ci));
    return true;
}

bool end_cmd_buffer(CmdBuffer &cmd) {
    VK_CHECK(vkEndCommandBuffer(cmd.handle));
    return true;
}

bool reset_cmd_buffer(CmdBuffer &cmd) {
    VK_CHECK(vkResetCommandBuffer(cmd.handle, 0));
    return true;
}

void begin_rendering(CmdBuffer &cmd, RenderingInfo &ri) {
    std::vector<VkRenderingAttachmentInfo> colors;
    colors.reserve(ri.colorAttachmentCount);

    VkRenderingInfo rendering_info{
        .sType = VK_STRUCTURE_TYPE_RENDERING_INFO,
        .renderArea = {{0, 0}, {ri.width, ri.height}},
        .layerCount = 1,
    };

    rendering_info.colorAttachmentCount = ri.colorAttachmentCount;
    for (u32 i = 0; i < ri.colorAttachmentCount; i++) {
        colors.push_back(to_vk_rendering_attachment(ri.colorAttachments[i]));
    }
    rendering_info.pColorAttachments = colors.data();

    VkRenderingAttachmentInfo depth_attach = {VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO};
    VkRenderingAttachmentInfo stencil_attach = {VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO};
    if (ri.depthAttachment.img != nullptr && ri.depthAttachment.img->_handle != nullptr) {
        auto aspects = ri.depthAttachment.img->desc.aspect;
        if (Any(aspects, ImageAspect::Depth)) {
            depth_attach = to_vk_rendering_attachment(ri.depthAttachment);
            rendering_info.pDepthAttachment = &depth_attach;
        }
        if (Any(aspects, ImageAspect::Stencil)) {
            stencil_attach = to_vk_rendering_attachment(ri.depthAttachment);
            rendering_info.pStencilAttachment = &stencil_attach;
        }
    }

    vkCmdBeginRendering(cmd.handle, &rendering_info);
    cmd.is_rendering = true;
}

void end_rendering(CmdBuffer &cmd) {
    vkCmdEndRendering(cmd.handle);
    cmd.is_rendering = false;
}

void set_viewports(CmdBuffer &cmd, Viewport *viewports, u32 viewportCount) {
    std::vector<VkViewport> vk_viewports = {};
    for (u32 i = 0; i < viewportCount; i++) {
        const Viewport &in = viewports[i];
        VkViewport out;
        out.x = in.x;
        out.y = in.y;
        out.width = in.width;
        out.height = in.height;
        out.minDepth = in.depth_min;
        out.maxDepth = in.depth_max;
        // flip for vulkan ndc
        if (in.flip_y) {
            out.y += in.height;
            out.height = -in.height;
        }
        vk_viewports.push_back(out);
    }
    vkCmdSetViewportWithCount(cmd.handle, viewportCount, vk_viewports.data());
};

void set_scissors(CmdBuffer &cmd, const Rect *rects, u32 rectCount) {
    std::vector<VkRect2D> vk_rects = {};
    for (u32 i = 0; i < rectCount; i++) {
        const Rect &in = rects[i];
        VkRect2D out;
        out.offset.x = in.x;
        out.offset.y = in.y;
        out.extent.width = in.width;
        out.extent.height = in.height;
        vk_rects.push_back(out);
    }
    vkCmdSetScissorWithCount(cmd.handle, rectCount, vk_rects.data());
}

void set_vertex_buffer(CmdBuffer &cmd, Buffer &buffer, u32 binding, u64 offset) {
    VkBuffer buffers[] = {buffer._handle};
    VkDeviceSize offsets[] = {offset};

    vkCmdBindVertexBuffers(cmd.handle, binding, 1, buffers, offsets);
}

void set_index_buffer(CmdBuffer &cmd, Buffer &buffer, u32 offset, IndexType type) {
    u64 size = buffer.desc.size - offset;
    vkCmdBindIndexBuffer(
        cmd.handle, buffer._handle, offset, type == IndexType::UINT32 ? VK_INDEX_TYPE_UINT32 : VK_INDEX_TYPE_UINT16
    );
}

void set_pipeline(CmdBuffer &cmd, RenderPipeline &pipeline) {
    VEL_ASSERT_HANDLE(cmd.handle, "SetPipeline: null command buffer");
    VEL_ASSERT_HANDLE(pipeline._handle, "SetPipeline: null pipeline handle");
    assert(pipeline.device != nullptr && "SetPipeline: pipeline has no device");
    vkCmdBindPipeline(cmd.handle, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline._handle);
    vkCmdBindDescriptorSets(
        cmd.handle,
        VK_PIPELINE_BIND_POINT_GRAPHICS,
        pipeline.device->pipeline_layout,
        0,
        1,
        &pipeline.device->descriptor_set,
        0,
        nullptr
    );
}

void set_pipeline(CmdBuffer &cmd, ComputePipeline &pipeline) {
    VEL_ASSERT_HANDLE(cmd.handle, "SetPipeline: null command buffer");
    VEL_ASSERT_HANDLE(pipeline._handle, "SetPipeline: null pipeline handle");
    assert(pipeline.device != nullptr && "SetPipeline: pipeline has no device");
    vkCmdBindPipeline(cmd.handle, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline._handle);
    vkCmdBindDescriptorSets(
        cmd.handle,
        VK_PIPELINE_BIND_POINT_COMPUTE,
        pipeline.device->pipeline_layout,
        0,
        1,
        &pipeline.device->descriptor_set,
        0,
        nullptr
    );
}

void set_constants(CmdBuffer &cmd, RenderPipeline &pipeline, rhi::ShaderStage stage, u32 size, const void *data) {
    (void)stage;
    assert(pipeline.device != nullptr && "SetConstants: pipeline has no device");
    vkCmdPushConstants(
        cmd.handle, pipeline.device->pipeline_layout, VK_SHADER_STAGE_ALL | VK_SHADER_STAGE_COMPUTE_BIT, 0, size, data
    );
}

void set_constants(CmdBuffer &cmd, ComputePipeline &pipeline, rhi::ShaderStage stage, u32 size, const void *data) {
    (void)stage;
    assert(pipeline.device != nullptr && "SetConstants: pipeline has no device");
    vkCmdPushConstants(
        cmd.handle, pipeline.device->pipeline_layout, VK_SHADER_STAGE_ALL | VK_SHADER_STAGE_COMPUTE_BIT, 0, size, data
    );
}

void draw(CmdBuffer &cmd, u32 vertexCount, u32 instanceCount, u32 firstVertex, u32 firstInstance) {
    vkCmdDraw(cmd.handle, vertexCount, instanceCount, firstVertex, firstInstance);
}

void draw_indexed(CmdBuffer &cmd, u32 indexCount, u32 instanceCount, u32 firstIndex, u32 vtxOff, u32 firstInstance) {
    vkCmdDrawIndexed(cmd.handle, indexCount, instanceCount, firstIndex, vtxOff, firstInstance);
}

void draw_indirect(CmdBuffer &cmd, Buffer &buffer, u32 drawCount, u32 stride, u64 offset) {
    vkCmdDrawIndirect(cmd.handle, buffer._handle, offset, drawCount, stride);
}

void draw_indirect_count(
    CmdBuffer &cmd, Buffer &buffer, Buffer &countBuffer, u32 drawCount, u32 stride, u64 offset, u32 countOffset
) {
    vkCmdDrawIndirectCount(cmd.handle, buffer._handle, offset, countBuffer._handle, countOffset, drawCount, stride);
}

void draw_indexed_indirect(
    CmdBuffer &cmd, Buffer &buffer, Buffer *countBuffer, u32 drawCount, u32 stride, u64 offset, u32 countOffset
) {
    if (countBuffer) {
        vkCmdDrawIndexedIndirectCount(
            cmd.handle, buffer._handle, offset, countBuffer->_handle, countOffset, drawCount, stride
        );
    } else {
        vkCmdDrawIndexedIndirect(cmd.handle, buffer._handle, offset, drawCount, stride);
    }
}

void fill_buffer(CmdBuffer &cmd, Buffer &buffer, u64 offset, u64 size, u32 value) {
    vkCmdFillBuffer(cmd.handle, buffer._handle, offset, size, value);
}

void dispatch(CmdBuffer &cmd, u32 x, u32 y, u32 z) {
    VEL_ASSERT_HANDLE(cmd.handle, "Dispatch: null command buffer");
    vkCmdDispatch(cmd.handle, x, y, z);
};

static bool state_is_read_only(ResourceState s) {
    switch (s) {
    case ResourceState::Idle:
    case ResourceState::ColorFetch:
    case ResourceState::DepthFetch:
    case ResourceState::TextureSample:
    case ResourceState::TextureSampleNonFragment:
    case ResourceState::StorageRead:
    case ResourceState::VertexFetch:
    case ResourceState::IndexFetch:
    case ResourceState::IndirectFetch:
    case ResourceState::UniformRead:
    case ResourceState::TransferFrom:
    case ResourceState::HostRead:
    case ResourceState::AccelTrace:
    case ResourceState::Display:
        return true;
    default:
        return false;
    }
}

static VkImageMemoryBarrier2
image_barrier_for(Image &image, const ImageRange &range, ResourceState before, ResourceState after) {
    vk::StateSync src = vk::image_state_to_vk(before, range.aspect);
    vk::StateSync dst = vk::image_state_to_vk(after, range.aspect);
    VkImageMemoryBarrier2 out{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2};
    out.srcStageMask = src.stages;
    out.srcAccessMask = src.access;
    out.dstStageMask = dst.stages;
    out.dstAccessMask = dst.access;
    out.oldLayout = src.layout == VK_IMAGE_LAYOUT_UNDEFINED && before == ResourceState::Idle ? VK_IMAGE_LAYOUT_UNDEFINED
                                                                                             : src.layout;
    out.newLayout = dst.layout;
    out.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    out.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    out.image = image._handle;
    out.subresourceRange = {
        vk::image_aspect_bit_to_vk(range.aspect),
        range.mip,
        (range.mip_count == REMAINING_MIPS) ? VK_REMAINING_MIP_LEVELS : range.mip_count,
        range.layer,
        (range.layer_count == REMAINING_LAYERS) ? VK_REMAINING_ARRAY_LAYERS : range.layer_count,
    };
    return out;
}

// no barriers inside rendering
static void require_outside_rendering(const CmdBuffer &cmd) {
    if (cmd.is_rendering) {
        VEL_CRITICAL("barrier() called inside dynamic rendering — hoist it before begin_rendering");
        std::abort();
    }
}

void barrier(CmdBuffer &cmd, ResourceState before, ResourceState after) {
    require_outside_rendering(cmd);
    vk::StateSync src = vk::buffer_state_to_vk(before);
    vk::StateSync dst = vk::buffer_state_to_vk(after);
    VkMemoryBarrier2 mem{VK_STRUCTURE_TYPE_MEMORY_BARRIER_2};
    mem.srcStageMask = src.stages;
    mem.srcAccessMask = src.access;
    mem.dstStageMask = dst.stages;
    mem.dstAccessMask = dst.access;
    VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO};
    dep.memoryBarrierCount = 1;
    dep.pMemoryBarriers = &mem;
    vkCmdPipelineBarrier2(cmd.handle, &dep);
}

void barrier(CmdBuffer &cmd, Buffer &buffer, ResourceState before, ResourceState after) {
    require_outside_rendering(cmd);
    if (before == after && before != ResourceState::Idle && state_is_read_only(before)) {
        return;
    }
    vk::StateSync src = vk::buffer_state_to_vk(before);
    vk::StateSync dst = vk::buffer_state_to_vk(after);
    VkBufferMemoryBarrier2 out{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2};
    out.srcStageMask = src.stages;
    out.srcAccessMask = src.access;
    out.dstStageMask = dst.stages;
    out.dstAccessMask = dst.access;
    out.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    out.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    out.buffer = buffer._handle;
    out.offset = 0;
    out.size = VK_WHOLE_SIZE;
    VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO};
    dep.bufferMemoryBarrierCount = 1;
    dep.pBufferMemoryBarriers = &out;
    vkCmdPipelineBarrier2(cmd.handle, &dep);
    buffer.state = after;
}

void barrier(CmdBuffer &cmd, Image &image, const ImageRange &range, ResourceState before, ResourceState after) {
    require_outside_rendering(cmd);
    if (before == after && before != ResourceState::Idle && state_is_read_only(before)) {
        return;
    }
    VkImageMemoryBarrier2 out = image_barrier_for(image, range, before, after);
    const bool region_local = cmd.is_rendering && after == ResourceState::ColorFetch;
    VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO};
    if (region_local) {
        dep.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
    }
    dep.imageMemoryBarrierCount = 1;
    dep.pImageMemoryBarriers = &out;
    vkCmdPipelineBarrier2(cmd.handle, &dep);
    if (range.mip_count == REMAINING_MIPS && range.layer_count == REMAINING_LAYERS) {
        image.state = after;
    }
}

static void copy_buffer_to_image(
    CmdBuffer &cmd, Buffer &srcBuffer, Image &dstImage, ImageAspect aspect, const BufferTextureCopyRegion &region
) {
    VkBuffer vk_src_buffer = srcBuffer._handle;
    VkImage vk_dst_image = dstImage._handle;

    VkBufferImageCopy2 copy_region{};
    copy_region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2;
    copy_region.bufferOffset = region.buffer_offset;
    copy_region.bufferRowLength = region.buffer_row_length;
    copy_region.bufferImageHeight = region.buffer_image_height;

    copy_region.imageSubresource.aspectMask = vk::image_aspect_to_vk(aspect);
    copy_region.imageSubresource.mipLevel = region.mip_level;
    copy_region.imageSubresource.baseArrayLayer = region.base_array_layer;
    copy_region.imageSubresource.layerCount = region.layer_count;

    copy_region.imageOffset = {
        static_cast<i32>(region.texture_offset_x),
        static_cast<i32>(region.texture_offset_y),
        static_cast<i32>(region.texture_offset_z)
    };

    copy_region.imageExtent = {
        static_cast<u32>(region.texture_extent_width),
        static_cast<u32>(region.texture_extent_height),
        static_cast<u32>(region.texture_extent_depth)
    };

    VkCopyBufferToImageInfo2 copy_info{};
    copy_info.sType = VK_STRUCTURE_TYPE_COPY_BUFFER_TO_IMAGE_INFO_2;
    copy_info.srcBuffer = vk_src_buffer;
    copy_info.dstImage = vk_dst_image;
    copy_info.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    copy_info.regionCount = 1;
    copy_info.pRegions = &copy_region;

    vkCmdCopyBufferToImage2(cmd.handle, &copy_info);
}

void copy_buffer_to_texture(
    CmdBuffer &cmd, Buffer &srcBuffer, ImageView &dstView, const BufferTextureCopyRegion &region
) {
    if (dstView.desc.image == nullptr || dstView.desc.image->_handle == 0) {
        VEL_CRITICAL("CopyBufferToTexture: invalid destination image view");
        return;
    }
    copy_buffer_to_image(cmd, srcBuffer, *dstView.desc.image, dstView.desc.aspect, region);
}

static void copy_image_to_buf(
    CmdBuffer &cmd, Image &srcImage, ImageAspect aspect, Buffer &dstBuffer, const rhi::BufferTextureCopyRegion &region
) {
    VkImage vk_src_image = srcImage._handle;
    VkBuffer vk_dst_buffer = dstBuffer._handle;

    VkBufferImageCopy2 copy_region{};
    copy_region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2;
    copy_region.bufferOffset = region.buffer_offset;
    copy_region.bufferRowLength = region.buffer_row_length;
    copy_region.bufferImageHeight = region.buffer_image_height;

    copy_region.imageSubresource.aspectMask = vk::image_aspect_to_vk(aspect);
    copy_region.imageSubresource.mipLevel = region.mip_level;
    copy_region.imageSubresource.baseArrayLayer = region.base_array_layer;
    copy_region.imageSubresource.layerCount = region.layer_count;

    copy_region.imageOffset = {
        static_cast<i32>(region.texture_offset_x),
        static_cast<i32>(region.texture_offset_y),
        static_cast<i32>(region.texture_offset_z)
    };

    copy_region.imageExtent = {
        static_cast<u32>(region.texture_extent_width),
        static_cast<u32>(region.texture_extent_height),
        static_cast<u32>(region.texture_extent_depth)
    };

    VkCopyImageToBufferInfo2 copy_info{};
    copy_info.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2;
    copy_info.srcImage = vk_src_image;
    copy_info.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
    copy_info.dstBuffer = vk_dst_buffer;
    copy_info.regionCount = 1;
    copy_info.pRegions = &copy_region;

    vkCmdCopyImageToBuffer2(cmd.handle, &copy_info);
}

void copy_image_to_buffer(
    CmdBuffer &cmd, ImageView &srcView, Buffer &dstBuffer, const rhi::BufferTextureCopyRegion &region
) {
    if (srcView.desc.image == nullptr || srcView.desc.image->_handle == 0) {
        VEL_CRITICAL("CopyImageToBuffer: invalid source image view");
        return;
    }
    copy_image_to_buf(cmd, *srcView.desc.image, srcView.desc.aspect, dstBuffer, region);
}

void copy_buffer_to_device(
    Device &device, Buffer &src, u64 src_offset, Buffer &dst, u64 dst_offset, u64 size, CmdBuffer &cmd
) {
    VkBufferCopy region{.srcOffset = src_offset, .dstOffset = dst_offset, .size = size};
    vkCmdCopyBuffer(cmd.handle, src._handle, dst._handle, 1, &region);
}

void blit_image(CmdBuffer &cmd, Image &image, const rhi::ImageBlit *regions, u32 region_count) {
    std::vector<VkImageBlit> vk_blits(region_count);
    for (u32 i = 0; i < region_count; ++i) {
        const rhi::ImageBlit &r = regions[i];
        vk_blits[i] = VkImageBlit{
            .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, r.src_mip, r.src_layer, 1},
            .srcOffsets = {{r.src_x0, r.src_y0, 0}, {r.src_x1, r.src_y1, 1}},
            .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, r.dst_mip, r.dst_layer, 1},
            .dstOffsets = {{r.dst_x0, r.dst_y0, 0}, {r.dst_x1, r.dst_y1, 1}},
        };
    }
    vkCmdBlitImage(
        cmd.handle,
        image._handle,
        VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
        image._handle,
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
        region_count,
        vk_blits.data(),
        VK_FILTER_LINEAR
    );
}

void set_polygon_mode(CmdBuffer &cmd, rhi::PipelineFillMode mode) {
    vkCmdSetPolygonModeEXT(cmd.handle, vk::pipeline_fill_mode_to_vk(mode));
}

void set_depth_write_enable(CmdBuffer &cmd, bool enable) {
    vkCmdSetDepthWriteEnableEXT(cmd.handle, enable ? VK_TRUE : VK_FALSE);
}

} // namespace rhi