Ch6-1 无图像帧缓冲

无图像帧缓冲(imageless framebuffer),是不指定image view的帧缓冲,它仅指定对图像附件的要求,而具体的image view则在渲染通道开始时指定。

无图像帧缓冲的(些微)好处显而易见:
原先,有几个会被用作渲染目标的图像就得有几个帧缓冲,代码量和需要管理的对象数量都显得冗余。而无图像帧缓冲可以适配任何符合要求的图像附件,交换链中的多张图像只需要有一个帧缓冲即可,而且交换链之外的图像只要能创建出符合要求的image view就也能用同一个帧缓冲。
(注:其实这东西卵用不大,直接看下一节也无妨)

本节在之前Ch2中绘制三角形代码的基础上,使用无图像帧缓冲搭配交换链图像进行渲染,以简单演示使用方法。

开启无图像帧缓冲设备特性

无图像帧缓冲是在Vulkan1.1.114版本中引入的功能,Vulkan1.2版本之前由设备级扩展"VK_KHR_imageless_framebuffer"提供(且依赖于其他扩展),从1.2版本开始为Vulkan核心功能的一部分。

取得Vulkan在本机上可用的最高版本,只有1.0.XXX的话直接返回。
Vulkan版本达到1.2的话,在前一节的代码里已经修改过graphicsBase::CreateDevice(...)了,并且该函数的逻辑是“能开的特性都开了”,所以只要检查设备特性是否支持即可:

int main() {
    graphicsBase::Base().UseLatestApiVersion();
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_1)
        //1.0.XXX的情况,在这里弹出信息窗口让用户升级显卡驱动吧!?
        return -1;
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_2) {
        /*1.1.XXX的情况,待填充*/
    }
    else
        if (!InitializeWindow({ 1280, 720 }) ||
            !graphicsBase::Base().PhysicalDeviceVulkan12Features().imagelessFramebuffer)
            return -1;

    /*...*/
}

Note

Vulkan版本和硬件支持
通过vkEnumerateInstanceVersion(...)取得的是Vulkan运行时(runtime)的版本,而非显卡驱动支持到的版本(稍微想想就明白,毕竟物理设备列表都得是创建Vulkan实例之后才取得的)。
运行时可以单独更新,但显卡驱动的支持也得更上。而更新显卡驱动时往往也会一并更新Vulkan运行时,所以如果用户的Vulkan运行时版本古旧,那这家伙的显卡驱动多半是很久没更新过了!

若Vulkan版本达到1.1但未到1.2,使用以下代码通过扩展开启:

int main() {
    graphicsBase::Base().UseLatestApiVersion();
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_1)
        return -1;
    if (graphicsBase::Base().ApiVersion() < VK_API_VERSION_1_2) {
        graphicsBase::Base().AddDeviceExtension(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);//Vulkan1.1.XXX需要这个扩展
        graphicsBase::Base().AddDeviceExtension(VK_KHR_IMAGELESS_FRAMEBUFFER_EXTENSION_NAME);
        VkPhysicalDeviceImagelessFramebufferFeatures physicalDeviceImagelessFramebufferFeatures = {
            VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGELESS_FRAMEBUFFER_FEATURES,
        };
        graphicsBase::Base().AddNextStructure_PhysicalDeviceFeatures(physicalDeviceImagelessFramebufferFeatures);
        if (!InitializeWindow({ 1280, 720 }) ||
            !physicalDeviceImagelessFramebufferFeatures.imagelessFramebuffer)
            return -1;
    }
    else
        if (!InitializeWindow({ 1280, 720 }) ||
            !graphicsBase::Base().PhysicalDeviceVulkan12Features().imagelessFramebuffer)
            return -1;

    /*...*/
}

创建无图像帧缓冲

原先是在easyVulkan::CreateRpwf_Screen(...)中创建的帧缓冲,我们就在其基础上进行修改。
先定义renderPassWithFramebuffer结构体,将渲染通道与其对应的帧缓冲放在一起以便于管理:

using namespace vulkan;
const VkExtent2D& windowSize = graphicsBase::Base().SwapchainCreateInfo().imageExtent;

namespace easyVulkan {
    struct renderPassWithFramebuffer {
        renderPass renderPass;
        framebuffer framebuffer;
    };
    const auto& CreateRpwf_Screen_ImagelessFramebuffer() {
        static renderPassWithFramebuffer rpwf;

        /*待后续填充*/

        return rpwf;
    }
};

这节不改渲染通道,照抄原先的即可:

const auto& CreateRpwf_Screen_ImagelessFramebuffer() {
    static renderPassWithFramebuffer rpwf;

    VkAttachmentDescription attachmentDescription = {
        .format = graphicsBase::Base().SwapchainCreateInfo().imageFormat,
        .samples = VK_SAMPLE_COUNT_1_BIT,
        .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
        .storeOp = VK_ATTACHMENT_STORE_OP_STORE,
        .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
        .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
    };
    VkAttachmentReference attachmentReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
    VkSubpassDescription subpassDescription = {
        .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
        .colorAttachmentCount = 1,
        .pColorAttachments = &attachmentReference
    };
    VkSubpassDependency subpassDependency = {
        .srcSubpass = VK_SUBPASS_EXTERNAL,
        .dstSubpass = 0,
        .srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
        .srcAccessMask = 0,
        .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
        .dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT
    };
    VkRenderPassCreateInfo renderPassCreateInfo = {
        .attachmentCount = 1,
        .pAttachments = &attachmentDescription,
        .subpassCount = 1,
        .pSubpasses = &subpassDescription,
        .dependencyCount = 1,
        .pDependencies = &subpassDependency
    };
    rpwf.renderPass.Create(renderPassCreateInfo);

    /*待后续填充*/

    return rpwf;
}

创建帧缓冲的部分还是要考虑重建交换链的情况,写lambda并设置为回调:

const auto& CreateRpwf_Screen_ImagelessFramebuffer() {
    static renderPassWithFramebuffer rpwf;

    VkAttachmentDescription attachmentDescription = { /*...*/ };
    VkAttachmentReference attachmentReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
    VkSubpassDescription subpassDescription = { /*...*/ };
    VkSubpassDependency subpassDependency = { /*...*/ };
    VkRenderPassCreateInfo renderPassCreateInfo = { /*...*/ };
    rpwf.renderPass.Create(renderPassCreateInfo);

    auto CreateFramebuffer = [] {
        /*待后续填充*/
        VkFramebufferCreateInfo framebufferCreateInfo = {
            .pNext = /*待填充*/,
            .flags = /*待填充*/,
            .renderPass = rpwf.renderPass,
            .attachmentCount = 1,
            .width = windowSize.width,
            .height = windowSize.height,
            .layers = 1
        };
        rpwf.framebuffer.Create(framebufferCreateInfo);
    };
    auto DestroyFramebuffer = [] {
        rpwf.framebuffer.~framebuffer();
    };
    CreateFramebuffer();

    ExecuteOnce(rpwf); //防止需重建逻辑设备时,重复添加回调函数
    graphicsBase::Base().AddCallback_CreateSwapchain(CreateFramebuffer);
    graphicsBase::Base().AddCallback_DestroySwapchain(DestroyFramebuffer);
    return rpwf;
}
  • 虽然没有图像附件了,VkFramebufferCreateInfo::pAttachments被无视(其值对创建帧缓冲无影响),但VkFramebufferCreateInfo::attachmentCount仍旧需要与渲染通道的图像附件数量匹配。

创建无图像帧缓冲时,VkFramebufferCreateInfo的pNext链中要包含一个VkFramebufferAttachmentsCreateInfo类型的结构体,并在VkFramebufferCreateInfo::flags中指定VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT

auto CreateFramebuffer = [] {
    /*待后续填充*/
    VkFramebufferAttachmentsCreateInfo framebufferAttachmentsCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_ATTACHMENTS_CREATE_INFO,
        /*待填充*/
    };
    VkFramebufferCreateInfo framebufferCreateInfo = {
        .pNext = &framebufferAttachmentsCreateInfo,
        .flags = VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT,
        .renderPass = rpwf.renderPass,
        .attachmentCount = 1,
        .width = windowSize.width,
        .height = windowSize.height,
        .layers = 1
    };
    rpwf.framebuffer.Create(framebufferCreateInfo);
};

struct VkFramebufferAttachmentsCreateInfo 的成员说明

VkStructureType sType

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

const void* pNext

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

uint32_t attachmentImageInfoCount

被描述的图像附件的数量,跟VkFramebufferCreateInfo::attachmentCount一致

const VkFramebufferAttachmentImageInfo* pAttachmentImageInfos

指向VkFramebufferAttachmentImageInfo的数组,用于描述对图像附件的要求

  • 根据VUID-VkFramebufferCreateInfo-flags-03191,在attachmentImageInfoCount不等于VkFramebufferCreateInfo::attachmentCount但是为0的情况下验证层不会报错,目前这一点似乎没有什么实际意义(谷歌一圈查不到任何解释)。

  • 如果你疑惑pAttachmentImageInfos所指数组中各个元素该是什么顺序,前情提要:渲染通道中与图像附件相关的参数(格式、采样点数、存读选项、内存布局)大都是在VkRenderPassCreateInfo::pAttachments中指定的,原先由VkFramebufferCreateInfo::pAttachments指定与之一一对应的具体图像附件。对于无图像帧缓冲自然是以此类推。

Note

Q:为什么要让attachmentImageInfoCount跟VkFramebufferCreateInfo::attachmentCount重复提供同一个数值?
A:我猜是因为API设计得考虑到这个结构今后可能在其他场合被单独使用(至少到撰写本文为止,VkFramebufferAttachmentsCreateInfo只会被VkFramebufferCreateInfo引用)。

struct VkFramebufferAttachmentImageInfo 的成员说明

VkStructureType sType

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

const void* pNext

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

VkImageCreateFlags flags

届时的图像附件,其对应的图像(VkImage)在被创建时被指定的flags

VkImageUsageFlags usage

届时的图像附件,其对应的图像(VkImage)在被创建时被指定的用途

uint32_t width

届时的图像附件的宽度

uint32_t height

届时的图像附件的高度

uint32_t layerCount

届时会被指定为图像附件的图像视图(VkImageView)的图层数

uint32_t viewFormatCount

见后文

const VkFormat* pViewFormats

见后文

  • flags和usage与创建相应图像时指定的VkImageCreateInfo::flags和VkImageCreateInfo::usage匹配。

  • 届时的图像附件,其对应的图像在被创建时,如果VkImageCreateInfo的pNext链中包含了VkImageFormatListCreateInfo,那么这里的pViewFormats所指数组为与VkImageFormatListCreateInfo中相同的格式列表,否则指向届时会被指定为图像附件的image view的格式即可(等价于创建渲染通道过程中指定的图像格式)。

VkImageFormatListCreateInfo是干什么的?
要讲这个,先前情提要一下可变格式图像:创建图像时,若VkImageCreateInfo::flags中注明VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT,则之后可以为图像创建不同格式的image view。
但是在一些平台上,可变格式图像的访问效率可能较差,因为驱动不清楚可能会以哪些格式的view来访问图像,就可能失去优化机会。
因此后来就有了VkImageFormatListCreateInfo,该类型结构体用以注明之后会为图像创建的image view会是哪些格式。
至于如果你要问,创建帧缓冲的时候注明这个干啥?底层的事情我也不知道!

于是来创建能与交换链图像搭配的无图像帧缓冲。
flags和usage怎么填?我们不是保存了交换链的创建信息嘛:

auto CreateFramebuffer = [] {
    VkFramebufferAttachmentImageInfo framebufferAttachmentImageInfo = {
        .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_ATTACHMENT_IMAGE_INFO,
        .usage = graphicsBase::Base().SwapchainCreateInfo().imageUsage,
        .width = /*待填充*/,
        .height = /*待填充*/,
        .layerCount = /*待填充*/,
        .viewFormatCount = /*待填充*/,
        .pViewFormats = /*待填充*/
    };
    VkFramebufferAttachmentsCreateInfo framebufferAttachmentsCreateInfo = {
        .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_ATTACHMENTS_CREATE_INFO,
        .attachmentImageInfoCount = 1,
        .pAttachmentImageInfos = &framebufferAttachmentImageInfo
    };
    VkFramebufferCreateInfo framebufferCreateInfo = {
        .pNext = &framebufferAttachmentsCreateInfo,
        .flags = VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT,
        .renderPass = rpwf.renderPass,
        .attachmentCount = 1,
        .width = windowSize.width,
        .height = windowSize.height,
        .layers = 1
    };
    rpwf.framebuffer.Create(framebufferCreateInfo);
};

然而突然意识到,VkSwapchainCreateInfo根本没有VkImageCreateFlags类型的成员!
好在我们当初创建交换链的时候也没有注明什么flags,这里的flags留作0就行了。。。
哎呀这可治标不治本,给你个VkSwapchainCreateFlagBitsKHRVkImageCreateFlagBits的转换列表吧:

版本要求

VkSwapchainCreateFlagBitsKHR 的枚举项

对应的VkImageCreateFlagBits

1.1 + VK_KHR_swapchain

VK_SWAPCHAIN_CREATE_SPLIT_INSTANCE_BIND_REGIONS_BIT_KHR

VK_IMAGE_CREATE_SPLIT_INSTANCE_BIND_REGIONS_BIT

1.1 + VK_KHR_swapchain

VK_SWAPCHAIN_CREATE_PROTECTED_BIT_KHR

VK_IMAGE_CREATE_PROTECTED_BIT

VK_KHR_swapchain_mutable_format

VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR

VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT出处

然后填写宽高和图层数。
图像附件是通过image view指定的,我们没有存交换链图像的image view的创建信息,但创建view时根本不会指定尺寸,view的尺寸等价于与其对应的图像。至于图层数,当然是1啦:

VkFramebufferAttachmentImageInfo framebufferAttachmentImageInfo = {
    .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_ATTACHMENT_IMAGE_INFO,
    .usage = graphicsBase::Base().SwapchainCreateInfo().imageUsage,
    //前情提要:windowSize即graphicsBase::Base().SwapchainCreateInfo().imageExtent
    .width = windowSize.width,
    .height = windowSize.height,
    .layerCount = 1,
    .viewFormatCount = /*待填充*/
    .pViewFormats = /*待填充*/
};
if (graphicsBase::Base().SwapchainCreateInfo().flags & VK_SWAPCHAIN_CREATE_SPLIT_INSTANCE_BIND_REGIONS_BIT_KHR)
    framebufferAttachmentImageInfo.flags |= VK_IMAGE_CREATE_SPLIT_INSTANCE_BIND_REGIONS_BIT;
if (graphicsBase::Base().SwapchainCreateInfo().flags & VK_SWAPCHAIN_CREATE_PROTECTED_BIT_KHR)
    framebufferAttachmentImageInfo.flags |= VK_IMAGE_CREATE_PROTECTED_BIT;
if (graphicsBase::Base().SwapchainCreateInfo().flags & VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR)
    framebufferAttachmentImageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT;

最后,既然我们没有把交换链图像指定为可变格式,更遑论写没写过VkImageFormatListCreateInfo,只要让pViewFormats指向交换链创建信息中的imageFormat便好:

VkFramebufferAttachmentImageInfo framebufferAttachmentImageInfo = {
    .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_ATTACHMENT_IMAGE_INFO,
    .usage = graphicsBase::Base().SwapchainCreateInfo().imageUsage,
    .width = windowSize.width,
    .height = windowSize.height,
    .layerCount = 1,
    .viewFormatCount = 1,
    .pViewFormats = &graphicsBase::Base().SwapchainCreateInfo().imageFormat
};
if (graphicsBase::Base().SwapchainCreateInfo().flags & VK_SWAPCHAIN_CREATE_SPLIT_INSTANCE_BIND_REGIONS_BIT_KHR)
    framebufferAttachmentImageInfo.flags |= VK_IMAGE_CREATE_SPLIT_INSTANCE_BIND_REGIONS_BIT;
if (graphicsBase::Base().SwapchainCreateInfo().flags & VK_SWAPCHAIN_CREATE_PROTECTED_BIT_KHR)
    framebufferAttachmentImageInfo.flags |= VK_IMAGE_CREATE_PROTECTED_BIT;
if (graphicsBase::Base().SwapchainCreateInfo().flags & VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR)
    framebufferAttachmentImageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT;

开始渲染通道

使用无图像帧缓冲的话,需要在开始渲染通道时,在VkRenderPassBeginInfo的pNext链中用VkRenderPassAttachmentBeginInfo指定具体的图像附件(没有任何难点):

struct VkRenderPassAttachmentBeginInfo 的成员说明

VkStructureType sType

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

const void* pNext

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

uint32_t attachmentCount

图像附件的数量

VkImageView pAttachments

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

int main() {
    if (!InitializeWindow({1280,720}))
        return -1;

    const auto& [renderPass, framebuffer] = RenderPassAndFramebuffers();
    CreateLayout();
    CreatePipeline();

    fence fence;
    semaphore semaphore_imageIsAvailable;
    semaphore semaphore_renderingIsOver;

    commandBuffer commandBuffer;
    commandPool commandPool(graphicsBase::Base().QueueFamilyIndex_Graphics(), VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT);
    commandPool.AllocateBuffers(commandBuffer);

    VkClearValue clearColor = { .color = { 1.f, 0.f, 0.f, 1.f } };

    while (!glfwWindowShouldClose(pWindow)) {
        while (glfwGetWindowAttrib(pWindow, GLFW_ICONIFIED))
            glfwWaitEvents();

        graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
        auto i = graphicsBase::Base().CurrentImageIndex();

        commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
        VkImageView attachment = graphicsBase::Base().SwapchainImageView(i);
        VkRenderPassAttachmentBeginInfo renderPassAttachmentBeginInfo = {
            .sType = VK_STRUCTURE_TYPE_RENDER_PASS_ATTACHMENT_BEGIN_INFO,
            .attachmentCount = 1,
            .pAttachments = &attachment
        };
        VkRenderPassBeginInfo renderPassBeginInfo = {
            .pNext = &renderPassAttachmentBeginInfo,
            .framebuffer = framebuffer,
            .renderArea = { {}, windowSize },
            .clearValueCount = 1,
            .pClearValues = &clearColor
        };
        renderPass.CmdBegin(commandBuffer, renderPassBeginInfo);

        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_triangle);
        vkCmdDraw(commandBuffer, 3, 1, 0, 0);

        renderPass.CmdEnd(commandBuffer);
        commandBuffer.End();

        graphicsBase::Base().SubmitCommandBuffer_Graphics(commandBuffer, semaphore_imageIsAvailable, semaphore_renderingIsOver, fence);
        graphicsBase::Base().PresentImage(semaphore_renderingIsOver);

        glfwPollEvents();
        TitleFps();

        fence.WaitAndReset();
    }
    TerminateWindow();
    return 0;
}

运行程序,渲染的结果不是重点(依旧是那个红背景和蓝三角形),验证层没报错便对了。
本节的示例代码参见:EasyVulkan.hppCh6-1.hpp