Ch3-4 渲染通道和帧缓冲

Render Pass

渲染通道(VkRenderPass)由图像附件(attachment)、子通道描述(subpass)、以及子通道依赖(VkSubpassDependency)组合而成。渲染通道指定渲染过程中,所绑定帧缓冲的参数(格式、内存布局)及各个渲染步骤之间的关系,换言之,渲染通道是对渲染流程的抽象。

创建渲染通道

vkCreateRenderPass(...)创建渲染通道:

VkResult VKAPI_CALL vkCreateRenderPass(...) 的参数说明

VkDevice device

逻辑设备的handle

const VkRenderPassCreateInfo* pCreateInfo

指向VkRenderPass的创建信息

const VkAllocationCallbacks* pAllocator

VkRenderPass* pRenderPass

若执行成功,将渲染通道的handle写入*pRenderPass

struct VkRenderPassCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkRenderPassCreateFlags flags

uint32_t attachmentCount

图像附件的数量

const VkAttachmentDescription* pAttachments

指向VkAttachmentDescription的数组,用于描述图像附件

uint32_t subpassCount

子通道的数量

const VkSubpassDescription* pSubpasses

指向VkSubpassDescription的数组,用于描述子通道

uint32_t dependenciesCount

子通道依赖的数量

const VkSubpassDependency* pDependencies

指向VkSubpassDependency的数组,用于描述子通道依赖

图像附件

通俗地说,图像附件就是渲染目标,图形命令向图像附件上输出颜色/深度/模板值。
VkAttachmentDescription描述图像附件:

struct VkAttachmentDescription 的成员说明

VkAttachmentDescriptionFlags flags

若填入VK_ATTACHMENT_DESCRIPTION_MAY_ALIAS_BIT,则说明该图像附件可能与其他图像附件共用内存

VkFormat format

图像附件的格式,与要被用作图像附件的image view的格式一致

VkSampleCountFlagBits samples

每个像素的采样点数量

VkAttachmentLoadOp loadOp

读取图像附件时,对颜色和深度值进行的操作

VkAttachmentStoreOp storeOp

存储颜色和深度值到图像附件时的操作,或指定不在乎存储

VkAttachmentLoadOp stencilLoadOp

读取图像附件时,对模板值进行的操作

VkAttachmentStoreOp stencilStoreOp

存储模板值到图像附件时的操作,或指定不在乎存储

VkImageLayout initialLayout

读取图像附件时的内存布局

VkImageLayout finalLayout

存储渲染结果到图像附件时,需转换至的内存布局

  • 自Vulkan1.0以来,flags中可设置VK_ATTACHMENT_DESCRIPTION_MAY_ALIAS_BIT,意为该图像附件可能与其他图像附件共用内存(但“作为某一图像附件写入后,再作为另一图像附件读取”这种行为通常是不安全的),某些情况下可以籍此节省一些内存。

  • samples决定了是否使用多重采样及其精度,若不使用多重采样或超采样,值应为VK_SAMPLE_COUNT_1_BIT,若使用4x多重采样或4x超采样(除指定samples外还需要其他设置,在此不做展开),值应为VK_SAMPLE_COUNT_4_BIT

VkAttachmentDescription只是用于描述图像附件,而不指定具体的图像附件或帧缓冲,因此其中不要求填写VkImageVkImageViewVkFramebuffer类型的handle。

读取图像附件这一行为,发生在该图像附件在渲染通道中首次被使用的时候,而存储图像附件则发生在该图像附件在渲染通道中最后一次被使用的时候。
首次使用某一图像附件未必发生在最初的子通道中。举例而言,实现延迟渲染时,首先在最初的子通道中渲染到G-buffer,然后在之后的子通道中渲染到交换链图像,于是交换链图像的首次使用发生在第二个子通道中。

Note

所谓“存储图像附件则发生在该图像附件在渲染通道中最后一次被使用的时候”,简而言之,storeOp仅影响渲染通道结束后如何存储生成的数值,而不影响中间过程,即,无论给被解析的多重采样颜色附件和输入附件(名词解释见后文)指定何种storeOp,它们都能正常发挥作用。

版本要求

VkAttachmentLoadOp 的枚举项

1.0

VK_ATTACHMENT_LOAD_OP_LOAD 表示读取图像附件时保留其原有内容

1.0

VK_ATTACHMENT_LOAD_OP_CLEAR 表示读取图像附件时清空其原有内容

1.0

VK_ATTACHMENT_LOAD_OP_DONT_CARE 表示不在乎读取图像附件时的操作(即图像附件的原有内容无关紧要)

版本要求

VkAttachmentStoreOp 的枚举项

1.0

VK_ATTACHMENT_STORE_OP_STORE 表示将渲染结果存入图像附件

1.0

VK_ATTACHMENT_STORE_OP_DONT_CARE 表示不在乎是否将渲染结果存入图像附件

1.3

VK_ATTACHMENT_STORE_OP_NONE 表示图像附件的渲染区域内不会发生写入(不是阻止写入渲染结果,而是你必须确保不产生渲染结果)

  • 显然,若不在渲染区域内进行渲染,或渲染结果被丢弃(GLSL着色器中使用discard),那么使用VK_ATTACHMENT_STORE_OP_STOREVK_ATTACHMENT_STORE_OP_NONE的结果是一致的,但后者可免去不必要的隐式同步。

关于内存布局,参见图像内存屏障。关于内存布局在渲染通道中的转换,见下一段。

子通道

既然渲染通道是对渲染流程的抽象,那么子通道就是渲染流程中的一个环节。
举例而言,直接渲染到屏幕缓冲(由交换链图像构成的帧缓冲)的做法只有一个子通道。而延迟渲染至少要经历两个子通道,一个生成G-buffer,一个用G-buffer在屏幕缓冲上进行光照计算,如果你还要做正向透明度的话,还得再多一个子通道。
VkSubpassDescription描述子通道中用到了哪些附件并各自起到何种作用:

struct VkSubpassDescription 的成员说明

VkPipelineBindPoint pipelineBindPoint

该通道对应的管线类型

uint32_t inputAttachmentCount

该子通道中所使用输入附件的数量

const VkAttachmentReference* pInputAttachments

指向VkAttachmentReference的数组,该数组引用该子通道所使用的输入附件

uint32_t colorAttachmentCount

该子通道中所使用颜色附件的数量

const VkAttachmentReference* pColorAttachments

指向VkAttachmentReference的数组,该数组引用该子通道所使用的颜色附件

const VkAttachmentReference* pResolveAttachments

指向VkAttachmentReference的数组,该数组引用该子通道所使用的解析附件,与颜色附件一一对应

const VkAttachmentReference* pDepthStencilAttachment

指向单个VkAttachmentReference对象,该对象引用该子通道所使用的深度模板附件

uint32_t preserveAttachmentCount

该子通道中保留附件的数量

const uint32_t* pPreserveAttachments

指向uint32_t的数组,该数组记录该子通道中的保留附件所对应VkRenderPassCreateInfo::pAttachments中元素的索引

  • 既然是渲染通道,那么VkSubpassDescription::pipelineBindPoint当然应该是VK_PIPELINE_BIND_POINT_GRAPHICS,这是不使用扩展的情况下的唯一选择。

struct VkAttachmentReference 的成员说明

uint32_t attachment

附件对应VkRenderPassCreateInfo::pAttachments中元素的索引

VkImageLayout imageLayout

该子通道内使用该附件时的内存布局

  • 内存布局转换如下发生:举例而言,若渲染通道有两个子通道,则渲染附件内存布局从VkAttachmentDescription::initialLayout变为第一个子通道中的布局,再变成第二个子通道中的布局,最后变为VkAttachmentDescription::finalLayout。

输入附件(input attachment)对于从OpenGL转来的程序员应该是个比较陌生的概念,其名称可能会让人误以为它指代该子通道中所用到的所有附件。事实上,它起到的是类似用于采样的贴图的作用,只不过输入附件必定是点对点的,读取输入附件时不采样,而是直接读取到与当前像素坐标同一位置的像素。
读取输入附件的效率比采样高,并且可以使用frambuffer-local依赖(见后文“子通道依赖”)。
输入附件的使用案例见Ch8-3 延迟渲染

颜色附件(color attachment)的概念应该不需要过多解释,令人疑惑的大概是为什么可以有多个颜色附件,事实上,你是可以在片段着色器中把颜色输出到多个颜色附件的,见图形着色器中通用的输入输出声明方式。注意所有颜色附件的采样次数都得是一致的。
可以一个颜色附件都没有,比如渲染阴影贴图,相应地也不需要片段着色器。

解析附件(resolve attachment)用于将多重采样的颜色附件解析为普通的图像,因此解析附件不得为多重采样的,即其samples必须为VK_SAMPLE_COUNT_1_BIT,此外每个解析附件的格式必须与相应颜色附件一致。解析是自动发生的。
深度模板附件也可以进行多重采样并参与解析,但Vulkan1.0版本不支持将多重采样的深度模板附件解析为独立的图像,如果你有这种需求,在Vulkan1.2中,可以使 VkSubpassDescription2::pNext指向一个VkSubpassDescriptionDepthStencilResolve结构体。

深度模板附件(depth stencil attachment)可以只有深度值或模板值,由格式决定。若既不进行深度测试也不进行模板测试,pDepthStencilAttachment为nullptr

保留附件(preserve attachment)说明该子通道不使用该附件,但必须保留其内容。举例而言,对于以下延迟渲染流程:G-buffer→延迟光照→渲染透明物体,需要三个子通道,而G-buffer环节中的深度信息不需要参与延迟光照,但需要保留到渲染透明物体的环节继续参与深度测试,于是必须将G-buffer子通道中的深度附件设定为延迟光照子通道中的保留附件。

一个子通道甚至整个渲染通道可以不使用任何图像附件,意在执行一些副作用,比方说在着色器中对storage缓冲区进行写入。

子通道依赖

子通道依赖是一种同步措施,它能确保附件的内存布局转换在正确的时机发生。
VkSubpassDependency描述子通道间的依赖条件,或子通道与外部的依赖条件:

struct VkSubpassDependency 的成员说明

uint32_t srcSubpass

源子通道,见后文

uint32_t dstSubpass

目标子通道,见后文

VkPipelineStageFlags srcStageMask

源管线阶段,见后文

VkPipelineStageFlags dstStageMask

目标管线阶段,见后文

VkAccessFlags srcAccessMask

源操作,见后文

VkAccessFlags dstAccessMask

目标操作,见后文

VkDependencyFlags dependencyFlags

自Vulkan1.0以来可以指定VK_DEPENDENCY_BY_REGION_BIT,见后文

  • srcSubpass与dstSubpass是子通道在整个渲染通道中的索引,即子通道对应的VkSubpassDescriptionVkRenderPassCreateInfo::pSubpasses所指数组中的索引。可对它们使用特殊值VK_SUBPASS_EXTERNAL,表示与当前渲染通道外的操作进行同步。

  • 关于VkPipelineStageFlagsVkAccessFlags,参见Pipeline Barrier(如果从Ch2-1点进这个页面,先不用管)。

  • 因为是mask,srcStageMask和dstStageMask中可以指定多于一个阶段,类似地,srcAccessMask和dstAccessMask中可以指定多于一种操作。

Vulkan官方标准中提到,Vulkan的命令按顺序开始执行,但未必按顺序结束,那么依序录制的命令的执行时间可能重叠。
记srcSubpass对应的子通道中,由srcStageMask所表示的阶段为第一同步域。
记dstSubpass对应的子通道中,由dstStageMask所表示的阶段为第二同步域。
子通道依赖确保:
1.第一同步域发生在第二同步域之前,且图像附件的内存布局转换发生在两者之间,这构成一种执行依赖
2.确保第一同步域中srcAccessMask所注明的任何写入操作的结果,能被第二同步域中dstAccessMask所注明的任何读取操作正确读取,这构成一种内存依赖
关于执行依赖和内存依赖的更多说明,见内存屏障的作用

Note

可以在srcAccessMask中填入诸如VK_ACCESS_COLOR_ATTACHMENT_READ_BIT之类的读操作,但没意义。

  • 在srcStageMask使用VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BITVK_PIPELINE_STAGE_ALL_COMMANDS_BIT,说明第二同步域的所有操作必须等待该队列执行完先前的所有命令。

  • 在dstStageMask上使用VK_PIPELINE_STAGE_TOP_OF_PIPE_BITVK_PIPELINE_STAGE_ALL_COMMANDS_BIT,说明之后在该队列上执行的所有命令都得在第一同步域的操作完成后执行。

版本要求

VkDependencyFlagBits 的枚举项

1.0

VK_DEPENDENCY_BY_REGION_BIT 表示该子通道依赖是framebuffer-local的

1.1

VK_DEPENDENCY_DEVICE_GROUP_BIT 表示该子通道依赖涉及到多个物理设备

1.1

VK_DEPENDENCY_VIEW_LOCAL_BIT 表示多视点的子通道dstSubpass中的每个视点依赖于srcSubpass中的单个(而不是所有或任意多个)视点,这称为view-local

什么是framebuffer-local的依赖?
指以帧缓冲区域(framebuffer region)为同步单位的依赖,单个帧缓冲区域为单个像素点或单个采样点。换言之,若需要同步的操作是点对点读写,那么便可将子通道依赖定义为framebuffer-local的,这种依赖称为帧缓冲空间依赖(framebuffer region dependency)。
定义帧缓冲空间依赖须满足:
1.你能确保第二同步域中的操作在访问图像的某个像素或采样点时,第一同步域中对该像素或采样点的操作已经完成了。
2.srcStageMask或dstStageMask中包含了以下四个阶段之中的至少一个:VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BITVK_PIPELINE_STAGE_FRAGMENT_SHADER_BITVK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BITVK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,上述阶段按发生的顺序排列,统称为帧缓冲空间阶段(framebuffer-space stage)。

什么叫涉及到多个物理设备?
使用多张显卡进行渲染。

什么是自依赖?
若某个子通道依赖中填入的srcSubpass和dstSubpass为同一子通道,则称该子通道是自依赖的。
若某个子通道依赖表明了一个自依赖的子通道,那么其srcStageMask和dstStageMask必须只包括图形管线阶段,
且如果srcStageMask包含任一帧缓冲空间阶段,那么dstStageMask必须只包含帧缓冲空间阶段,如此一来将构成帧缓冲空间依赖(此时必须在dependencyFlags中指定VK_DEPENDENCY_BY_REGION_BIT)。

每个子通道开始和结束时各应有一个子通道依赖。
整个渲染通道开始和结束时具有隐式的子通道依赖,可以手动书写覆盖这两个隐式依赖。这两个隐式依赖被定义为:

//渲染通道开始时的隐式依赖
VkSubpassDependency implicitDependency_renderPassBegin = {
    .srcSubpass = VK_SUBPASS_EXTERNAL,
    .dstSubpass = firstSubpass, //首个使用了图像附件的子通道
    .srcStageMask = VK_PIPELINE_STAGE_NONE,
    .dstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
    .srcAccessMask = 0,
    .dstAccessMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT |
                     VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
                     VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
                     VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
                     VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
    .dependencyFlags = 0,
};
//渲染通道结束时的隐式依赖
VkSubpassDependency implicitDependency_renderPassEnd = {
    .srcSubpass = lastSubpass, //最后一个使用了图像附件的子通道
    .dstSubpass = VK_SUBPASS_EXTERNAL,
    .srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
    .dstStageMask = VK_PIPELINE_STAGE_NONE,
    .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
                     VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
    .dstAccessMask = 0,
    .dependencyFlags = 0,
};
  • 如果你要覆盖渲染通道开始时的隐式依赖,srcSubpass必须为VK_SUBPASS_EXTERNAL,dstSubpass必须为firstSubpass。同理,如果你要覆盖渲染通道开始时的隐式依赖,srcSubpass必须为lastSubpass,dstSubpass必须为VK_SUBPASS_EXTERNAL

开始渲染通道

所谓开始渲染通道,就是开始该渲染通道所代表的一系列渲染流程。
在命令缓冲区中,用vkCmdBeginRenderPass(...)开始一个渲染通道:

void VKAPI_CALL vkCmdBeginRenderPass(...) 的参数说明

VkCommandBuffer commandBuffer

正在录制的命令缓冲区的handle

const VkRenderPassBeginInfo* pRenderPassBegin

指向渲染通道的开始信息

VkSubpassContents contents

若为VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS,则最初的子通道的内容将由二级命令缓冲区提供

struct VkRenderPassBeginInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkRenderPass renderPass

渲染通道的handle

VkFramebuffer framebuffer

所使用的帧缓冲的handle

VkRect2D renderArea

渲染区域

uint32_t clearValueCount

清屏值的数量

const VkClearValue pClearValues

指向清屏值的数组

  • renderArea.offset(起始点)和renderArea.extent(大小)皆以像素为单位,renderArea.offset的坐标以画面左上角为原点,向右向下为正(不考虑呈现引擎对图像做的变换的话)。

  • 清屏值的个数至少要涵盖到VkRenderPassCreateInfo::pAttachments中最后一个需要被清屏(loadOp为VK_ATTACHMENT_LOAD_OP_CLEAR)的图像附件。比如,若VkRenderPassCreateInfo::pAttachments指向三个元素的数组,且只有其中第二个图像附件需要清屏,那么clearValueCount应当至少为2。

进入下一个子通道

在命令缓冲区中,用vkCmdNextSubpass(...)进入下一个子通道:

void VKAPI_CALL vkCmdNextSubpass(...) 的参数说明

VkCommandBuffer commandBuffer

正在录制的命令缓冲区的handle

VkSubpassContents contents

若为VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS,则接下来的子通道的内容将由二级命令缓冲区提供

  • vkCmdBeginRenderPass(...)的情况一样,若contents为VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS,则直到本子通道结束前只能执行二级命令缓冲区。

结束渲染通道

在命令缓冲区中,用vkCmdEndRenderPass(...)结束当前渲染通道:

void VKAPI_CALL vkCmdEndRenderPass(...) 的参数说明

VkCommandBuffer commandBuffer

正在录制的命令缓冲区的handle

封装为renderPass类

VKBase.h,vulkan命名空间中添加以下代码:

class renderPass {
    VkRenderPass handle = VK_NULL_HANDLE;
public:
    renderPass() = default;
    renderPass(VkRenderPassCreateInfo& createInfo) {
        Create(createInfo);
    }
    renderPass(renderPass&& other) noexcept { MoveHandle; }
    ~renderPass() { DestroyHandleBy(vkDestroyRenderPass); }
    //Getter
    DefineHandleTypeOperator;
    DefineAddressFunction;
    //Const Function
    void CmdBegin(VkCommandBuffer commandBuffer, VkRenderPassBeginInfo& beginInfo, VkSubpassContents subpassContents = VK_SUBPASS_CONTENTS_INLINE) const {
        beginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
        beginInfo.renderPass = handle;
        vkCmdBeginRenderPass(commandBuffer, &beginInfo, subpassContents);
    }
    void CmdBegin(VkCommandBuffer commandBuffer, VkFramebuffer framebuffer, VkRect2D renderArea, arrayRef<const VkClearValue> clearValues = {}, VkSubpassContents subpassContents = VK_SUBPASS_CONTENTS_INLINE) const {
        VkRenderPassBeginInfo beginInfo = {
            .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
            .renderPass = handle,
            .framebuffer = framebuffer,
            .renderArea = renderArea,
            .clearValueCount = uint32_t(clearValues.Count()),
            .pClearValues = clearValues.Pointer()
        };
        vkCmdBeginRenderPass(commandBuffer, &beginInfo, subpassContents);
    }
    void CmdNext(VkCommandBuffer commandBuffer, VkSubpassContents subpassContents = VK_SUBPASS_CONTENTS_INLINE) const {
        vkCmdNextSubpass(commandBuffer, subpassContents);
    }
    void CmdEnd(VkCommandBuffer commandBuffer) const {
        vkCmdEndRenderPass(commandBuffer);
    }
    //Non-const Function
    result_t Create(VkRenderPassCreateInfo& createInfo) {
        createInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
        VkResult result = vkCreateRenderPass(graphicsBase::Base().Device(), &createInfo, nullptr, &handle);
        if (result)
            outStream << std::format("[ renderPass ] ERROR\nFailed to create a render pass!\nError code: {}\n", int32_t(result));
        return result;
    }
};
  • VK_SUBPASS_CONTENTS_INLINE值为0。

Framebuffer

帧缓冲(VkFramebuffer)是用于特定渲染流程的一系列图像附件的集合。

创建帧缓冲

vkCreateFramebuffer(...)创建帧缓冲:

VkResult VKAPI_CALL vkCreateFramebuffer(...) 的参数说明

VkDevice device

逻辑设备的handle

const VkFramebufferCreateInfo* pCreateInfo

指向VkFramebuffer的创建信息

const VkAllocationCallbacks* pAllocator

VkRenderPass* pFramebuffer

若创建成功,将帧缓冲的handle写入*pFramebuffer

struct VkFramebufferCreateInfo 的成员说明

VkStructureType sType

结构体的类型,本处必须是VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO

const void* pNext

如有必要,指向一个用于扩展该结构体的结构体

VkFramebufferCreateFlags flags

uint32_t attachmentCount

图像附件的数量

const VkImageView* pAttachments

指向VkImageView的数组,用于指定图像附件

uint32_t width

帧缓冲的宽度,不得为0,若非创建无图像帧缓冲,必须小于等于图像附件的宽度

uint32_t height

帧缓冲的高度,不得为0,若非创建无图像帧缓冲,必须小于等于图像附件的高度

uint32_t layers

帧缓冲的图层数,不得为0,若非创建无图像帧缓冲,必须小于等于图像附件的图层数

  • VkFramebufferCreateInfo::pAttachments与VkRenderPassCreateInfo::pAttachments一一对应。这两者类似于实参与形参的关系。

  • 注意,图像附件指代图像视图(VkImageView),因此图像附件的图层数指的是图像视图的图层数。对于图像附件的宽和高,则不必分辨是图像还是图像视图的宽高,因为创建图像视图时不指定宽高,图像视图的宽高与对应图像的宽高等价。

  • 自Vulkan1.2起,可以在flags中指定VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT,并使pNext指向一个VkFramebufferAttachmentsCreateInfo结构体,以创建无图帧缓冲(imageless framebuffer),这种帧缓冲仅指定图像附件的大小、图层数和图像格式,而实际的图像附件则在渲染通道开始时再行指定,详见Ch6-1 无图像帧缓冲

如果提供的VkImageView是2D图像数组,那么创建的帧缓冲称为多层帧缓冲(layered framebuffer),这有时候会非常有用,但有些硬件(比如英特尔核显)不支持多层帧缓冲。多层帧缓冲的应用实例,参考://TODO 将Erp图像转为Cubemap

封装为framebuffer类

VKBase.h,vulkan命名空间中添加以下代码:

class framebuffer {
    VkFramebuffer handle = VK_NULL_HANDLE;
public:
    framebuffer() = default;
    framebuffer(VkFramebufferCreateInfo& createInfo) {
        Create(createInfo);
    }
    framebuffer(framebuffer&& other) noexcept { MoveHandle; }
    ~framebuffer() { DestroyHandleBy(vkDestroyFramebuffer); }
    //Getter
    DefineHandleTypeOperator;
    DefineAddressFunction;
    //Non-const Function
    result_t Create(VkFramebufferCreateInfo& createInfo) {
        createInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
        VkResult result = vkCreateFramebuffer(graphicsBase::Base().Device(), &createInfo, nullptr, &handle);
        if (result)
            outStream << std::format("[ framebuffer ] ERROR\nFailed to create a framebuffer!\nError code: {}\n", int32_t(result));
        return result;
    }
};