Ch7-6 拷贝图像到屏幕

本节的main.cpp对应示例代码中的:Ch7-6.hpp

由于到成功渲染出图像为止有诸多步骤,本套教程中分两步走。
本节学习如何使用stb_image读取图像数据、如何将图像数据拷贝到设备内存,以及blit命令。
阅读本节前请确保已阅读Ch3-2中关于image的说明

将图像拷贝到交换链图像的流程

将图像数据直接拷贝或blit到交换链图像,是通过Vulkan在屏幕上显示内容的最快方式。
当你的程序有大量的需要加载或创建的资源时,你可以通过这一方法来显示所谓的“启动画面”,展示一张诸如你的LOGO之类的图像。

以下是将图像数据拷贝到交换链图像的流程:
1.读取图像数据到CPU内存
2.将图像数据从CPU内存拷贝到暂存缓冲区
接下来,根据图像尺寸与交换链图像是否相同,以及图像格式是否相同,有两个分支:
(A:若图像与交换链图像尺寸、格式相同)
A-3.将图像数据从暂存缓冲区直接拷贝到交换链图像
(B:若图像与交换链图像尺寸、格式不同)
B-3.获取与暂存缓冲区混叠的暂存图像,若无法混叠,将图像数据从暂存缓冲区直接拷贝到一张新建的暂存图像
B-4.将图像数据从暂存图像blit到交换链图像

读取图像数据

考虑读取图像的两种情形:
1.直接从硬盘读取文件
2.从内存读取文件数据
如果你想把所有东西(图像、音频等会在你的程序里用到的资源)都打包进单个可执行程序,或把资源扔进dll,那么得先用平台特定的函数从程序自身或dll读取资源,获得资源的内存地址。然后再从内存读取并解析文件数据。

为了省去重复的代码,我打算将两种情形统一到同一个模板函数中,然后根据参数类型应用运行期分支。
VKBase+.h中,vulkan命名空间中加入类texture,并定义受保护静态成员函数LoadFile_Internal(...):

class texture {
protected:
    static std::unique_ptr<uint8_t[]> LoadFile_Internal(
        const auto* address,             //字符串指针,或资源的内存地址
        size_t fileSize,                 //文件大小,当且仅当传入的是资源的内存地址时有需要
        VkExtent2D& extent,              //图像大小,将由stb_image的函数写入
        formatInfo requiredFormatInfo) { //对读取所得数据的格式要求
#ifndef NDEBUG
        /*待填充*/
#endif
        void* pImageData = nullptr;//用于接收读取到的图像数据
        //运行期分支:若传入的address是文件路径(字符串)
        if constexpr (std::same_as<decltype(address), const char*>) {
            /*待填充*/
        }
        //运行期分支:若传入的address是内存地址
        if constexpr (std::same_as<decltype(address), const char*>) {
            /*待填充*/
        }
        return std::unique_ptr<uint8_t[]>(static_cast<uint8_t*>(pImageData));
    }
};

除了pImageData外,stb_image的读图函数还会要求以下参数:

//用于接收长和宽,extent.width和extent.height是uint32_t,因此需要转换
int& width = reinterpret_cast<int&>(extent.width);
int& height = reinterpret_cast<int&>(extent.height);
//用于接收原始色彩通道数,读完图后我用不着它,所以也没有将其初始化
int channelCount;

有些格式要求是无法满足的,因此加入在Debug Build下检查requiredFormatInfo的代码:

#ifndef NDEBUG
if (//若要求数据为浮点数,stb_image只支持32位浮点数
    (requiredFormatInfo.rawDataType == formatInfo::floatingPoint && requiredFormatInfo.sizePerComponent == 4) ||
    //若要求数据为整形,stb_image只支持8位或16位每通道
    (requiredFormatInfo.rawDataType == formatInfo::integer && requiredFormatInfo.sizePerComponent >= 1 && requiredFormatInfo.sizePerComponent <= 2))
    /*空表达式*/;
else
    outStream << std::format("[ texture ] ERROR\nRequired format is not available for source image data!\n"),
    abort();
#endif

做掉else分支,则目前为止LoadFile_Internal(...)为:

static std::unique_ptr<uint8_t[]> LoadFile_Internal(const auto* address, size_t fileSize, VkExtent2D& extent, formatInfo requiredFormatInfo) {
#ifndef NDEBUG
    if (!(requiredFormatInfo.rawDataType == formatInfo::floatingPoint && requiredFormatInfo.sizePerComponent == 4) &&
        !(requiredFormatInfo.rawDataType == formatInfo::integer && requiredFormatInfo.sizePerComponent && requiredFormatInfo.sizePerComponent <= 2))
        outStream << std::format("[ texture ] ERROR\nRequired format is not available for source image data!\n"),
        abort();
#endif
    int& width = reinterpret_cast<int&>(extent.width);
    int& height = reinterpret_cast<int&>(extent.height);
    int channelCount;
    void* pImageData = nullptr;
    if constexpr (std::same_as<decltype(address), const char*>) {
        /*待填充*/
    }
    if constexpr (std::same_as<decltype(address), const uint8_t*>) {
        /*待填充*/
    }
    return std::unique_ptr<uint8_t[]>(static_cast<uint8_t*>(pImageData));
}
  • std::same_as是一个C++概念(concept),根据运行期的类型判断被转为相应布尔值,若类型一致则为真。若对C++概念不熟悉,请自行参阅相关文档。

stbi_load(...)将图像文件读取为8位整形序列:

STBIDEF stbi_uc* stbi_load(...) 的参数说明

const char* filename

文件路径

int* x

输出图像宽到*x

int* y

输出图像宽到*y

int* comp

输出图像色彩通道数到*comp

int req_comp

若非0,图像色彩通道数会被减少或补齐到req_comp

  • 该函数自动识别文件类型,支持bmp、tga、png、jpeg、hdr等常见格式(具体请自行参见文件内注释)。如果你事先确定文件类型,也可以直接用诸如stbi__tga_load(...)之类具体格式的读取函数。

  • 若函数执行成功,返回读取所得数据的内存地址,否则为nullptr

类似地,用stbi_load_16(...)将图像读取为16位整形序列,用stbi_loadf(...)将图像读取为32位浮点数序列,参数与stbi_load(...)一致。
于是,根据requiredFormatInfo中的各种要求,有以下分支:

if (requiredFormatInfo.rawDataType == formatInfo::integer)
    if (requiredFormatInfo.sizePerComponent == 1)
        pImageData = stbi_load(address, &width, &height, &channelCount, requiredFormatInfo.componentCount);
    else
        pImageData = stbi_load_16(address, &width, &height, &channelCount, requiredFormatInfo.componentCount);
else
    pImageData = stbi_loadf(address, &width, &height, &channelCount, requiredFormatInfo.componentCount);
if (!pImageData)
    outStream << std::format("[ texture ] ERROR\nFailed to load the file: {}\n", address);

从内存读取文件数据时,步骤几乎一致,差别仅在于:要提供文件数据的大小,以及各个读取函数替换成对应的有_from_memory后缀的版本:

STBIDEF stbi_uc* stbi_load_from_memory(...) 的参数说明

const stbi_uc* buffer

文件数据的内存地址

int len

文件数据的大小

int* x

输出图像宽到*x

int* y

输出图像宽到*y

int* comp

输出图像色彩通道数到*comp

int req_comp

若非0,图像色彩通道数会被减少或补齐到req_comp

既然len的类型是int,那么显然,文件数据不能达到2G。
最后整个LoadFile_Internal(...)如下:

static std::unique_ptr<uint8_t[]> LoadFile_Internal(const auto* address, size_t fileSize, VkExtent2D& extent, formatInfo requiredFormatInfo) {
#ifndef NDEBUG
    if (!(requiredFormatInfo.rawDataType == formatInfo::floatingPoint && requiredFormatInfo.sizePerComponent == 4) &&
        !(requiredFormatInfo.rawDataType == formatInfo::integer && Between_Closed<int32_t>(1, requiredFormatInfo.sizePerComponent, 2)))
        outStream << std::format("[ texture ] ERROR\nRequired format is not available for source image data!\n"),
        abort();
#endif
    int& width = reinterpret_cast<int&>(extent.width);
    int& height = reinterpret_cast<int&>(extent.height);
    int channelCount;
    void* pImageData = nullptr;
    if constexpr (std::same_as<decltype(address), const char*>) {
        if (requiredFormatInfo.rawDataType == formatInfo::integer)
            if (requiredFormatInfo.sizePerComponent == 1)
                pImageData = stbi_load(address, &width, &height, &channelCount, requiredFormatInfo.componentCount);
            else
                pImageData = stbi_load_16(address, &width, &height, &channelCount, requiredFormatInfo.componentCount);
        else
            pImageData = stbi_loadf(address, &width, &height, &channelCount, requiredFormatInfo.componentCount);
        if (!pImageData)
            outStream << std::format("[ texture ] ERROR\nFailed to load the file: {}\n", address);
    }
    if constexpr (std::same_as<decltype(address), const uint8_t*>) {
        if (fileSize > INT32_MAX) {
            outStream << std::format("[ texture ] ERROR\nFailed to load image data from the given address! Data size must be less than 2G!\n");
            return {};
        }
        if (requiredFormatInfo.rawDataType == formatInfo::integer)
            if (requiredFormatInfo.sizePerComponent == 1)
                pImageData = stbi_load_from_memory(address, fileSize, &width, &height, &channelCount, requiredFormatInfo.componentCount);
            else
                pImageData = stbi_load_16_from_memory(address, fileSize, &width, &height, &channelCount, requiredFormatInfo.componentCount);
        else
            pImageData = stbi_loadf_from_memory(address, fileSize, &width, &height, &channelCount, requiredFormatInfo.componentCount);
        if (!pImageData)
            outStream << std::format("[ texture ] ERROR\nFailed to load image data from the given address!\n");
    }
    return std::unique_ptr<uint8_t[]>(static_cast<uint8_t*>(pImageData));
}
  • 这里为了让代码不太难看,使用了Between_Closed(...)来替代先前相同的代码逻辑,该函数见示例代码中的EasyVKStart.h

如果你需要根据图片的通道数channelCount及你指定的其他参数确定对应的VkFormat,自行修改函数。会有些麻烦,需要写一张映射表。

注意到我给上述函数起的名字有_Internal后缀,你可能已经意识到了我接下来要干什么:

class texture {
protected:
    static std::unique_ptr<uint8_t[]> LoadFile_Internal(const auto* address, size_t fileSize, VkExtent2D& extent, formatInfo requiredFormatInfo) {
        /*...*/
    }
public:
    [[nodiscard]]
    static std::unique_ptr<uint8_t[]> LoadFile(const char* filepath, VkExtent2D& extent, formatInfo requiredFormatInfo) {
        return LoadFile_Internal(filepath, 0, extent, requiredFormatInfo);
    }
    [[nodiscard]]
    static std::unique_ptr<uint8_t[]> LoadFile(const uint8_t* fileBinaries, size_t fileSize, VkExtent2D& extent, formatInfo requiredFormatInfo) {
        return LoadFile_Internal(fileBinaries, fileSize, extent, requiredFormatInfo);
    }
};

顺便,提供在Windows上从exe或dll读取资源的函数。
关于以下代码具体的含义,以及Visual Studio中如何打包资源,因与Vulkan无关,请自行搜索相关教程。

std::pair<const uint8_t*, size_t> LoadResourceFromModule(int32_t resourceId, HMODULE hModule = NULL) {
    if (HRSRC hResource = FindResource(hModule, MAKEINTRESOURCE(resourceId), RT_RCDATA))
        if (HGLOBAL hData = LoadResource(hModule, hResource))
            if (const uint8_t* pData = static_cast<uint8_t*>(LockResource(hData)))
                return { pData, SizeofResource(hModule, hResource) };
    return {};
}

BootScreen

我们得有一个函数来放这一节接下来要写的东西。
于是乎,既然本节的目标就只是拷贝图像到屏幕,那么如我前面所说的“启动画面”这一非常简单的应用场景就非常合适。
EasyVulkan.hpp,easyVulkan命名空间中加入:

void BootScreen(const char* imagePath, VkFormat imageFormat) {
    VkExtent2D imageExtent;
    std::unique_ptr<uint8_t[]> pImageData = texture2d::LoadFile(imagePath, imageExtent, FormatInfo(imageFormat));
    if (!pImageData)
        return;//这里我就偷个懒,不写错误信息了
    /*待填充*/
}

接着先来学习下前面学过的东西。
于是我现在不需要构建渲染循环,只需要提交一次命令缓冲区就够了,不用考虑之后的事,且命令缓冲区中不需要渲染通道。
那么需要哪些对象,并调用哪些函数呢?如果你尚不能信手拈来地写一个渲染循环,那么我建议你在此先回忆一下。
...
显然需要命令缓冲区,因为已经直接写在了题设里。
...
录制、提交并等待命令缓冲区需要配一个栅栏。
...
既然要用到交换链图像,那么肯定得先获得它。在我的封装中,这一步用graphicsBase::SwapImage(...)进行。
...
于是,函数加入如下内容:

void BootScreen(const char* imagePath, VkFormat imageFormat) {
    //用先前写的函数从硬盘读图
    VkExtent2D imageExtent;
    std::unique_ptr<uint8_t[]> pImageData = texture2d::LoadFile(imagePath, imageExtent, FormatInfo(imageFormat));
    if (!pImageData)
        return;//这里我就偷个懒,不写错误信息了
    /*待填充*/

    //创建同步对象
    semaphore semaphore_imageIsAvailable;
    fence fence;
    //分配命令缓冲区
    commandBuffer commandBuffer;
    graphicsBase::Plus().CommandPool_Graphics().AllocateBuffers(commandBuffer);

    //获取交换链图像
    graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);

    //录制命令缓冲区
    commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
    /*待填充*/
    commandBuffer.End();

    //提交命令缓冲区
    VkPipelineStageFlags waitDstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    VkSubmitInfo submitInfo = {
        .waitSemaphoreCount = 1,
        .pWaitSemaphores = semaphore_imageIsAvailable.Address(),
        .pWaitDstStageMask = &waitDstStage,
        .commandBufferCount = 1,
        .pCommandBuffers = commandBuffer.Address()
    };
    graphicsBase::Base().SubmitCommandBuffer_Graphics(submitInfo, fence);
    //等待命令执行完毕
    fence.WaitAndReset();
    //呈现图像
    graphicsBase::Base().PresentImage();

    //别忘了释放命令缓冲区
    graphicsBase::Plus().CommandPool_Graphics().FreeBuffers(commandBuffer);
}
  • 因为之后会使用的拷贝和Blit命令都是传输命令,所以waitDstStageVK_PIPELINE_STAGE_TRANSFER_BIT

  • 因为这里的应用场景不要求同步粒度,因此我直接在呈现图像前等待fence,而不必使用第二个信号量。

然后我们将内存中的图像数据存入暂存缓冲区:

void BootScreen(const char* imagePath, VkFormat imageFormat) {
    VkExtent2D imageExtent;
    std::unique_ptr<uint8_t[]> pImageData = texture2d::LoadFile(imagePath, imageExtent, FormatInfo(imageFormat));
    if (!pImageData)
        return;
    /*新增*/stagingBuffer::BufferData_MainThread(pImageData.get(), FormatInfo(imageFormat).sizePerPixel * imageExtent.width * imageExtent.height);

    semaphore semaphore_imageIsAvailable;
    fence fence;
    commandBuffer commandBuffer;
    graphicsBase::Plus().CommandPool_Graphics().AllocateBuffers(commandBuffer);

    graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);

    commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
    /*待填充*/
    commandBuffer.End();

    VkPipelineStageFlags waitDstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    VkSubmitInfo submitInfo = {
        .waitSemaphoreCount = 1,
        .pWaitSemaphores = semaphore_imageIsAvailable.Address(),
        .pWaitDstStageMask = &waitDstStage,
        .commandBufferCount = 1,
        .pCommandBuffers = commandBuffer.Address()
    };
    graphicsBase::Base().SubmitCommandBuffer_Graphics(submitInfo, fence);
    fence.WaitAndReset();
    graphicsBase::Base().PresentImage();

    graphicsBase::Plus().CommandPool_Graphics().FreeBuffers(commandBuffer);
}

接着就是确定分支了:能否从暂存缓冲区直接拷贝到交换链图像,还是说需要blit?

commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);

VkExtent2D swapchainImageSize = graphicsBase::Base().SwapchainCreateInfo().imageExtent;
bool blit =
    imageExtent.width != swapchainImageSize.width ||                       //宽
    imageExtent.height != swapchainImageSize.height ||                     //高
    imageFormat != graphicsBase::Base().SwapchainCreateInfo().imageFormat; //图像格式

    /*这里还缺点东西,待填充*/
    if (blit) {
        /*需要blit的分支,待填充*/
    }
    else {
        /*直接copy的分支,待填充*/
    }

commandBuffer.End();

图像操作

VKBase+.h中,vulkan命名空间中加入结构体或类imageOperation。这个类型没有成员也不会创建实例,相比在命名空间中书写全局函数,非常理所当然地,我更倾向于在类中写静态成员函数。
然后定义一个imageOperation的内部类型imageMemoryBarrierParameterPack和两个静态成员函数:

struct imageOperation {
    struct imageMemoryBarrierParameterPack {
        /*待填充*/
    };
    static void CmdCopyBufferToImage(VkCommandBuffer commandBuffer, VkBuffer buffer, VkImage image, const VkBufferImageCopy& region,
        imageMemoryBarrierParameterPack imb_from, imageMemoryBarrierParameterPack imb_to) {
        /*待填充*/
    }
    static void CmdBlitImage(VkCommandBuffer commandBuffer, VkImage image_src, VkImage image_dst, const VkImageBlit& region,
        imageMemoryBarrierParameterPack imb_dst_from, imageMemoryBarrierParameterPack imb_dst_to, VkFilter filter = VK_FILTER_LINEAR) {
        /*待填充*/
    }
};

顾名思义,imageOperation是图像操作相关的类,除了本节中用于从缓冲区将数据拷贝到图像的CmdCopyBufferToImage(...),以及将数据从图像blit到另一图像的CmdBlitImage(...)外,之后还会有生成mipmap的函数,这些函数都将会非常实用。
其中imageMemoryBarrierParameterPack是用于指定管线屏障及图像内存屏障的参数集合,其定义如下:

struct imageMemoryBarrierParameterPack {
    const bool isNeeded = false;                            //是否需要屏障,默认为false
    const VkPipelineStageFlags stage = 0;                   //srcStages或dstStages
    const VkAccessFlags access = 0;                         //srcAccessMask或dstAccessMask
    const VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; //oldLayout或newLayout
    //默认构造器,isNeeded保留为false
    constexpr imageMemoryBarrierParameterPack() = default;
    //若指定参数,三个参数必须被全部显示指定,isNeeded被赋值为true
    constexpr imageMemoryBarrierParameterPack(VkPipelineStageFlags stage, VkAccessFlags access, VkImageLayout layout) :
        isNeeded(true), stage(stage), access(access), layout(layout) {}
};
  • 指定构造器为constexpr以便后续创建可被常量初始化的静态常量(涉及一些简化代码的技巧,会在Ch5-2用到)。

在Vulkan 1.3中stage为0(VK_PIPELINE_STAGE_NONE)是有效值,VK_IMAGE_LAYOUT_UNDEFINED的值是0,那么即便stage、access、layout三者皆为0,也未必构成无效参数。
因此不以三者是否全部为0来判断,而是用一个额外的成员isNeeded来标定是否需要管线屏障。如果不需要,则对传入CmdCopyBufferToImage(...)和CmdBlitImage(...)的相应参数使用{}默认初始化即可。

CmdCopyBufferToImage

来简单看一下我为CmdCopyBufferToImage规定的各个参数:

static void CmdCopyBufferToImage(
    VkCommandBuffer commandBuffer,   //命令缓冲区的handle
    VkBuffer buffer,                 //作为数据来源的缓冲区
    VkImage image,                   //接收数据的目标图像
    const VkBufferImageCopy& region, //指定将缓冲区的哪些部分拷贝到图像的哪些部分
    imageMemoryBarrierParameterPack imb_from, //后述
    imageMemoryBarrierParameterPack imb_to) {
        /*待填充*/
}

从一个VkBuffer拷贝数据到一个VkImage,通常需要考虑该操作前后是否需要管线屏障,因此我们初步将CmdCopyBufferToImage变为:

static void CmdCopyBufferToImage(VkCommandBuffer commandBuffer, VkBuffer buffer, VkImage image, const VkBufferImageCopy& region,
    imageMemoryBarrierParameterPack imb_from, imageMemoryBarrierParameterPack imb_to) {
    //若拷贝前需要管线屏障
    if (imb_from.isNeeded)
        /*待填充*/
    //调用拷贝命令
    vkCmdCopyBufferToImage(/*待填充*/);
    //若拷贝后需要管线屏障
    if (imb_to.isNeeded)
        /*待填充*/
}

如果你还没有阅读本套教程中Pipeline Barrier,那么是时候了。

该解释imb_fromimb_to这两个参数的含义了,注意我用的后缀分别是_from和_to,“从”和“到”。
执行vkCmdCopyBufferToImage(...)命令时:

  • 图像的内存布局应当被变成最适用于传输命令的VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL

  • 拷贝发生的管线阶段为VK_PIPELINE_STAGE_TRANSFER_BIT

  • 拷贝对应的操作类型为VK_ACCESS_TRANSFER_WRITE_BIT

于是,拷贝前的管线屏障旨在要求:先于该屏障的命令中由imb_from.stage所指示的阶段(第一个同步域),必须在该屏障后的vkCmdCopyBufferToImage(...)命令执行到VK_PIPELINE_STAGE_TRANSFER_BIT阶段(第二个同步域)前完成,即拷贝发生前完成。
以防你没有仔细阅读管线屏障相关的说明,再次强调,简而言之,管线屏障注明其前后的命令在执行时,有哪些阶段不能重叠。
图像的内存布局会在两个同步域间,由imb_from.layout转到VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
此处的内存屏障还应当确保传输命令的写入操作(VK_ACCESS_TRANSFER_WRITE_BIT),在由imb_from.access指明的、先前的写入操作的结果的基础上发生。

于是拷贝前的管线屏障为:

//这个结构体不放if底下,后面改改还要用
VkImageMemoryBarrier imageMemoryBarrier = {
    VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
    nullptr,
    imb_from.access,
    VK_ACCESS_TRANSFER_WRITE_BIT,
    imb_from.layout,
    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
    VK_QUEUE_FAMILY_IGNORED, //无队列族所有权转移
    VK_QUEUE_FAMILY_IGNORED,
    image,
    {
        region.imageSubresource.aspectMask,
        region.imageSubresource.mipLevel,
        1,
        region.imageSubresource.baseArrayLayer,
        region.imageSubresource.layerCount }
};
if (imb_from.isNeeded)
    vkCmdPipelineBarrier(commandBuffer, imb_from.stage, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
        0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);

类似地,你该也能举一反三地明白拷贝后的图像管线屏障的意义了。简单修改imageMemoryBarrier后,书写拷贝后的管线屏障:

if (imb_to.isNeeded) {
    imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    imageMemoryBarrier.dstAccessMask = imb_to.access;
    imageMemoryBarrier.newLayout = imb_to.layout;
    vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, imb_to.stage, 0,
        0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
}

接着来看看重点(但没什么难点)的vkCmdCopyBufferToImage(...):

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

VkCommandBuffer commandBuffer

命令缓冲区的handle

VkBuffer srcBuffer

源缓冲区

VkImage dstImage

目标图像

VkImageLayout dstImageLayout

目标图像的内存布局

uint32_t regionCount

要被拷贝的数据块的个数

const VkBufferImageCopy* pRegions

指向VkBufferImageCopy类型的数组,用于指定要被拷贝的数据块

struct VkBufferImageCopy 的成员说明

VkDeviceSize bufferOffset

要被拷贝的数据在源缓冲区中的起始位置,或数据被拷贝到目标缓冲区的起始位置,单位是字节

uint32_t bufferRowLength

将缓冲区从bufferOffset起始的部分视作图像,在此填入其宽度,单位是像素

uint32_t bufferImageHeight

将缓冲区从bufferOffset起始的部分视作图像,在此填入其高度,单位是像素

VkImageSubresourceLayers imageSubresource

图像的子资源范围

VkOffset3D imageOffset

要被拷贝的图像区域,或复制入数据的图像区域的起始位置,单位是像素

VkExtent3D imageExtent

要被拷贝的图像区域,或复制入数据的图像区域的大小,单位是像素

  • 若bufferRowLength和bufferImageHeight都非0,那么无论是从缓冲区拷贝数据到图像,或从图像拷贝数据到缓冲区,bufferRowLength和bufferImageHeight都应该大于等于imageExtent指定的长宽。

  • bufferRowLength大于imageExtent.width意味着有每行的数据之间有空余字节而非紧密排列,若bufferRowLength * bufferImageHeight > imageExtent.width * imageExtent.length则说明每个切面(可以拷贝自/拷贝到3D图像)之间有空余字节。

  • 若bufferRowLength和bufferImageHeight之一为0,等效于两者等于imageExtent指定的长宽,则复制自缓冲区的图像数据之间为紧密排列,而从图像复制进缓冲区的数据之间则不会补足空余字节。

struct VkImageSubresourceLayers 的成员说明

VkImageAspectFlags aspectMask

所使用图像的层面

uint32_t mipLevel

mip等级

uint32_t baseArrayLayer

初始图层

uint32_t layerCount

图层总数

于是补全CmdCopyBufferToImage(...)如下:

static void CmdCopyBufferToImage(VkCommandBuffer commandBuffer, VkBuffer buffer, VkImage image, const VkBufferImageCopy& region,
    imageMemoryBarrierParameterPack imb_from, imageMemoryBarrierParameterPack imb_to) {
    VkImageMemoryBarrier imageMemoryBarrier = {
        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        nullptr,
        imb_from.access,
        VK_ACCESS_TRANSFER_WRITE_BIT,
        imb_from.layout,
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
        VK_QUEUE_FAMILY_IGNORED, //无队列族所有权转移
        VK_QUEUE_FAMILY_IGNORED,
        image,
        {
            region.imageSubresource.aspectMask,
            region.imageSubresource.mipLevel,
            1,
            region.imageSubresource.baseArrayLayer,
            region.imageSubresource.layerCount }
    };
    if (imb_from.isNeeded)
        vkCmdPipelineBarrier(commandBuffer, imb_from.stage, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    vkCmdCopyBufferToImage(commandBuffer, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
    if (imb_to.isNeeded) {
        imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
        imageMemoryBarrier.dstAccessMask = imb_to.access;
        imageMemoryBarrier.newLayout = imb_to.layout;
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, imb_to.stage, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    }
}

CmdBlitImage

来简单看一下我为CmdBlitImage规定的各个参数:

static void CmdBlitImage(
    VkCommandBuffer commandBuffer, //命令缓冲区的handle
    VkImage image_src,             //作为数据来源的图像
    VkImage image_dst,             //接收数据的目标图像
    const VkImageBlit& region,     //指定将源图像哪些部分拷贝到目标图像的哪些部分
    imageMemoryBarrierParameterPack imb_dst_from,
    imageMemoryBarrierParameterPack imb_dst_to,
    VkFilter filter = VK_FILTER_LINEAR) { //若图像可能被放缩,在此指定滤波方式
}
  • imb_dst_from和imb_dst_to与CmdCopyBufferToImage(...)的imb_from和imb_to作用一致。Blit涉及两张图像,本函数只处理目标图像的同步,这里为避免歧义而添加了后缀。

本函数不将源图像用作内存屏障的同步对象,一是出于将其用法设计得与CmdCopyBufferToImage(...)类似的想法。
二是,在遇到要调用本函数的情况时,源图像的内存布局转换等,很可能由本函数前后的其他过程完成。比如,流水线中要将一张离屏渲染好的图像作为源图像,blit到交换链图像(像是,出于性能原因,只有UI以屏幕分辨率渲染,游戏主画面以较低分辨率渲染的情况),那么可以直接用渲染通道结束时的子通道依赖进行内存布局转换。

接着,类似我们之前所做的,先将CmdBlitImage(...)分成三个部分,并写好除了变量名外与CmdCopyBufferToImage(...)中完全一致的管线屏障:

static void CmdBlitImage(VkCommandBuffer commandBuffer, VkImage image_src, VkImage image_dst, const VkImageBlit& region,
    imageMemoryBarrierParameterPack imb_dst_from, imageMemoryBarrierParameterPack imb_dst_to, VkFilter filter = VK_FILTER_LINEAR) {
    VkImageMemoryBarrier imageMemoryBarrier = {
        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        nullptr,
        imb_dst_from.access,
        VK_ACCESS_TRANSFER_WRITE_BIT,
        imb_dst_from.layout,
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
        VK_QUEUE_FAMILY_IGNORED,
        VK_QUEUE_FAMILY_IGNORED,
        image,
        {
            region.imageSubresource.aspectMask,
            region.imageSubresource.mipLevel,
            1,
            region.imageSubresource.baseArrayLayer,
            region.imageSubresource.layerCount }
    };
    if (imb_dst_from.isNeeded)
        vkCmdPipelineBarrier(commandBuffer, imb_dst_from.stage, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    vkCmdBlitImage(/*待填充*/);
    if (imb_dst_to.isNeeded) {
        imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
        imageMemoryBarrier.dstAccessMask = imb_dst_to.access;
        imageMemoryBarrier.newLayout = imb_dst_to.layout;
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, imb_dst_to.stage, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    }
}

接着来看一下vkCmdBlitImage(...)的用法:

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

VkCommandBuffer commandBuffer

命令缓冲区的handle

VkImage srcImage

源图像

VkImageLayout srcImageLayout

源图像的内存布局

VkImage dstImage

目标图像

VkImageLayout dstImageLayout

目标图像的内存布局

uint32_t regionCount

要被blit的数据块的个数

const VkImageBlit* pRegions

指向VkImageBlit类型的数组,用于指定要被blit的数据块

VkFilter filter

图像被放缩时的滤波方式,关于滤波方式,见Ch3-7 采样器

struct VkImageBlit 的成员说明

VkImageSubresourceLayers srcSubresource

源图像的子资源范围

VkOffset3D srcOffsets[2]

源图像被blit的数据的边界

VkImageSubresourceLayers dstSubresource

目标图像的子资源范围

VkOffset3D dstOffsets[2]

目标图像被blit入数据的边界

  • srcOffsets[0]的各分量未必要小于srcOffsets[1]的各分量,dstOffsets也是同理。

值得一提的是,blit命令不仅能放缩,通过妥当设置srcOffset,还能实现图像翻转,比如:

//若这是无翻转
VkImageBlit region_blit = {
    { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
    { {}, { width, height, 1 } },
    { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
    { {}, { width, height, 1 } }
};
//那么这是横向翻转
VkImageBlit region_blit = {
    { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
    { { width }, { 0, height, 1 } },
    { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
    { {}, { width, height, 1 } }
};

源图像的内存布局当然该是VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,于是补全CmdBlitImage(...)如下:

static void CmdBlitImage(VkCommandBuffer commandBuffer, VkImage image_src, VkImage image_dst, const VkImageBlit& region,
    imageMemoryBarrierParameterPack imb_dst_from, imageMemoryBarrierParameterPack imb_dst_to, VkFilter filter = VK_FILTER_LINEAR) {
    VkImageMemoryBarrier imageMemoryBarrier = {
        VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        nullptr,
        imb_dst_from.access,
        VK_ACCESS_TRANSFER_WRITE_BIT,
        imb_dst_from.layout,
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
        VK_QUEUE_FAMILY_IGNORED,
        VK_QUEUE_FAMILY_IGNORED,
        image,
        {
            region.imageSubresource.aspectMask,
            region.imageSubresource.mipLevel,
            1,
            region.imageSubresource.baseArrayLayer,
            region.imageSubresource.layerCount }
    };
    if (imb_dst_from.isNeeded)
        vkCmdPipelineBarrier(commandBuffer, imb_dst_from.stage, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    vkCmdBlitImage(commandBuffer,
        image_src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
        image_dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
        1, &region, filter);
    if (imb_dst_to.isNeeded) {
        imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
        imageMemoryBarrier.dstAccessMask = imb_dst_to.access;
        imageMemoryBarrier.newLayout = imb_dst_to.layout;
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, imb_dst_to.stage, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    }
}

从暂存缓冲区到交换链图像

先来处理较为简单的分支。
于是乎,调用我们所写的CmdCopyBufferToImage(...):

if (blit) {
    /*待填充*/
}
else {
    VkBufferImageCopy region_copy = {
        /*待填充*/
    };
    imageOperation::CmdCopyBufferToImage(commandBuffer,
        stagingBuffer::Buffer_MainThread(),
        graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex()),
        region_copy,
        { /*待填充*/ },
        { /*待填充*/ });
}

先前调用了stagingBuffer::BufferData_MainThread(...)将来源图像的数据紧密地存入了暂存缓冲区起始的位置。因而region_copy.bufferOffsetregion_copy.bufferRowLengthregion_copy.bufferImageHeight皆为0。
图像数据被拷入交换链图像的起始位置,因而region_copy.imageOffset皆0。
这个分支的前提是,所使用图像大小与交换链图像一致,于是region_copy为:

VkBufferImageCopy region_copy = {
    .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
    .imageExtent = { imageExtent.width, imageExtent.height, 1 }
};

显然,因为这个函数旨在于程序开始时显示图像,所以交换链图像尚未被使用过,在管线屏障中没有需要被阻塞的命令,srcStageMask填写为VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,则srcAccessMask必须为0。图像的初始内存布局则是VK_IMAGE_LAYOUT_UNDEFINED
而之后,因为会用栅栏来同步,便不需要通过管线屏障来堵住后续命令的管线阶段。也无需注明访问操作的类型来确保交换链图像的资源可见性,因为虽然等待栅栏不会确保资源可见性,但是vkQueuePresentKHR(...)会,于是只需要指定最后的内存布局是VK_IMAGE_LAYOUT_PRESENT_SRC_KHR即可。

if (blit) {
    /*待填充*/
}
else {
    VkBufferImageCopy region_copy = {
        .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
        .imageExtent = { imageExtent.width, imageExtent.height, 1 }
    };
    imageOperation::CmdCopyBufferToImage(commandBuffer,
        stagingBuffer::Buffer_MainThread(),
        graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex()),
        region_copy,
        { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
        { VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR  });
}

管线屏障是Vulkan学习过程中的一个难点,但会经常使用。
往下阅读之前,请确保你已经理解了此处管线屏障和图像内存屏障的作用。

从暂存缓冲区到暂存图像

接着是较为复杂的分支。
我们首先试着创建能够与暂存缓冲区混叠的暂存图像:

/*待填充*/
if (blit) {
    VkImage image = stagingBuffer::AliasedImage2d_MainThread(imageFormat, imageExtent);
    /*待填充*/
}
else {
    /*...*/
}

你应该已经读过stagingBuffer::AliasedImage2d(...)的用法了,若这里image为VK_NULL_HANDLE,那么说明无法创建与暂存缓冲区混叠的图像,这可能有各种原因,比如图像大小不是某些数值的倍数,所以得有填充字节等等(而我们存入暂存缓冲区的数据当然是紧密排列的)。
无论如何,这时我们就得创建一张有独立设备内存的暂存图像,然后将图像数据从暂存缓冲区拷贝至其中。
于是产生两个分支:

/*待填充*/
if (blit) {
    VkImage image = stagingBuffer::AliasedImage2d_MainThread(imageFormat, imageExtent);
    if (image) {
        /*待填充*/
    }
    else {
        /*待填充*/
    }
    /*待填充*/
}
else {
    /*...*/
}

先来处理image非0的分支,只需使用图像内存屏障,在正确的时机转换内存布局即可。
与暂存缓冲区混叠的图像,其内存布局为VK_IMAGE_LAYOUT_PREINITIALIZED,转换到VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
没有要被阻塞的命令,srcStageMask为VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,srcAccessMask为0。注意没必要写VK_PIPELINE_STAGE_HOST_BITVK_ACCESS_HOST_WRITE_BIT,用vkQueueSubmit(...)提交命令缓冲区便足以确保资源可见性。
而dstStageMask为VK_PIPELINE_STAGE_TRANSFER_BIT,dstAccessMask当然是VK_ACCESS_TRANSFER_READ_BIT

/*待填充*/
if (blit) {
    VkImage image = stagingBuffer::AliasedImage2d_MainThread(imageFormat, imageExtent);
    if (image) {
        VkImageMemoryBarrier imageMemoryBarrier = {
            VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
            nullptr,
            0,
            VK_ACCESS_TRANSFER_READ_BIT,
            VK_IMAGE_LAYOUT_PREINITIALIZED,
            VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
            VK_QUEUE_FAMILY_IGNORED,
            VK_QUEUE_FAMILY_IGNORED,
            image,
            { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
        };
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    }
    else {
        /*待填充*/
    }
    /*待填充*/
}
else {
    /*...*/
}

处理另一分支。于是来创建一张新的图像,因为数据会从暂存缓冲区复制进去,然后再从其中复制到交换链图像,因此图像用途同时包含VK_IMAGE_USAGE_TRANSFER_SRC_BITVK_IMAGE_USAGE_TRANSFER_DST_BIT,内存属性为最适合物理设备访问的VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,其他参数应当无须说明:

imageMemory imageMemory;
if (blit) {
    VkImage image = stagingBuffer::AliasedImage2d_MainThread(imageFormat, imageExtent);
    if (image) {
        VkImageMemoryBarrier imageMemoryBarrier = {
            VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
            nullptr,
            0,
            VK_ACCESS_TRANSFER_READ_BIT,
            VK_IMAGE_LAYOUT_PREINITIALIZED,
            VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
            VK_QUEUE_FAMILY_IGNORED,
            VK_QUEUE_FAMILY_IGNORED,
            image,
            { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
        };
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    }
    else {
        VkImageCreateInfo imageCreateInfo = {
            .imageType = VK_IMAGE_TYPE_2D,
            .format = imageFormat,
            .extent = { imageExtent.width, imageExtent.height, 1 },
            .mipLevels = 1,
            .arrayLayers = 1,
            .samples = VK_SAMPLE_COUNT_1_BIT,
            .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT
        };
        imageMemory.Create(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
        /*待填充*/
    }
    /*待填充*/
}
else {
    /*...*/
}
  • 为确保代表新图像的对象imageMemory在提交并执行完命令前不被析构,必须将其定义在一堆分支的语句块之前。

接着调用CmdCopyBufferToImage(...)将数据从暂存缓冲区拷贝进去,图像内存屏障需要确保在被之后的blit命令读取前完成内存布局转换,参数也就不言而喻了:

imageMemory imageMemory;
if (blit) {
    VkImage image = stagingBuffer::AliasedImage2d_MainThread(imageFormat, imageExtent);
    if (image) {
        VkImageMemoryBarrier imageMemoryBarrier = {
            VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
            nullptr,
            0,
            VK_ACCESS_TRANSFER_READ_BIT,
            VK_IMAGE_LAYOUT_PREINITIALIZED,
            VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
            VK_QUEUE_FAMILY_IGNORED,
            VK_QUEUE_FAMILY_IGNORED,
            image,
            { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
        };
        vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
    }
    else {
        VkImageCreateInfo imageCreateInfo = {
            .imageType = VK_IMAGE_TYPE_2D,
            .format = imageFormat,
            .extent = { imageExtent.width, imageExtent.height, 1 },
            .mipLevels = 1,
            .arrayLayers = 1,
            .samples = VK_SAMPLE_COUNT_1_BIT,
            .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT
        };
        imageMemory.Create(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
        VkBufferImageCopy region_copy = {
            .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
            .imageExtent = imageCreateInfo.extent
        };
        imageOperation::CmdCopyBufferToImage(commandBuffer,
            stagingBuffer::Buffer_MainThread(),
            imageMemory.Image(),
            region_copy,
            { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
            { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL });

        //将image赋值为imageMemory.Image()以便后续操作
        image = imageMemory.Image();
    }
    /*待填充*/
}
else {
    /*...*/
}

从暂存图像blit到交换链图像

现在,image一定记录了一个有效的handle。 调用我们所写的CmdBlitImage(...)将图像数据从image中blit到交换链图像:

if (blit) {
    VkImage image = stagingBuffer::AliasedImage2d_MainThread(imageFormat, imageExtent);
    if (image) {
        /*...*/
    }
    else {
        /*...*/
    }
    VkImageBlit region_blit = {
        { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
        { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } },
        { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
        { {}, { int32_t(swapchainImageSize.width), int32_t(swapchainImageSize.height), 1 } }
    };
    imageOperation::CmdBlitImage(commandBuffer,
        image,
        graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex()),
        region_blit,
        { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
        { VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR }, VK_FILTER_LINEAR);
}
else {
    /*...*/
}

根据你的需要指定滤波方式。
最后,整个BootScreen(...)如下:

void BootScreen(const char* imagePath, VkFormat imageFormat) {
    VkExtent2D imageExtent;
    std::unique_ptr<uint8_t[]> pImageData = texture2d::LoadFile(imagePath, imageExtent, FormatInfo(imageFormat));
    if (!pImageData)
        return;
    stagingBuffer::BufferData_MainThread(pImageData.get(), FormatInfo(imageFormat).sizePerPixel * imageExtent.width * imageExtent.height);

    semaphore semaphore_imageIsAvailable;
    fence fence;
    commandBuffer commandBuffer;
    graphicsBase::Plus().CommandPool_Graphics().AllocateBuffers(commandBuffer);

    graphicsBase::Base().SwapImage(semaphore_imageIsAvailable);
    commandBuffer.Begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
    VkExtent2D swapchainImageSize = graphicsBase::Base().SwapchainCreateInfo().imageExtent;
    bool blit =
        imageExtent.width != swapchainImageSize.width ||
        imageExtent.height != swapchainImageSize.height ||
        imageFormat != graphicsBase::Base().SwapchainCreateInfo().imageFormat;
    imageMemory imageMemory;
    if (blit) {
        VkImage image = stagingBuffer::AliasedImage2d_MainThread(imageFormat, imageExtent);
        if (image) {
            VkImageMemoryBarrier imageMemoryBarrier = {
                VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                nullptr,
                0,
                VK_ACCESS_TRANSFER_READ_BIT,
                VK_IMAGE_LAYOUT_PREINITIALIZED,
                VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                VK_QUEUE_FAMILY_IGNORED,
                VK_QUEUE_FAMILY_IGNORED,
                image,
                { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }
            };
            vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &imageMemoryBarrier);
        }
        else {
            VkImageCreateInfo imageCreateInfo = {
                .imageType = VK_IMAGE_TYPE_2D,
                .format = imageFormat,
                .extent = { imageExtent.width, imageExtent.height, 1 },
                .mipLevels = 1,
                .arrayLayers = 1,
                .samples = VK_SAMPLE_COUNT_1_BIT,
                .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT
            };
            imageMemory.Create(imageCreateInfo, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
            VkBufferImageCopy region_copy = {
                .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
                .imageExtent = imageCreateInfo.extent
            };
            imageOperation::CmdCopyBufferToImage(commandBuffer,
                stagingBuffer::Buffer_MainThread(),
                imageMemory.Image(),
                region_copy,
                { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
                { VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL });

            image = imageMemory.Image();
        }
        VkImageBlit region_blit = {
            { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
            { {}, { int32_t(imageExtent.width), int32_t(imageExtent.height), 1 } },
            { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
            { {}, { int32_t(swapchainImageSize.width), int32_t(swapchainImageSize.height), 1 } }
        };
        imageOperation::CmdBlitImage(commandBuffer,
            image,
            graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex()),
            region_blit,
            { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
            { VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR }, VK_FILTER_LINEAR);
    }
    else {
        VkBufferImageCopy region_copy = {
            .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
            .imageExtent = { imageExtent.width, imageExtent.height, 1 }
        };
        imageOperation::CmdCopyBufferToImage(commandBuffer,
            stagingBuffer::Buffer_MainThread(),
            graphicsBase::Base().SwapchainImage(graphicsBase::Base().CurrentImageIndex()),
            region_copy,
            { VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_UNDEFINED },
            { VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR  });
    }
    commandBuffer.End();

    VkPipelineStageFlags waitDstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    VkSubmitInfo submitInfo = {
        .waitSemaphoreCount = 1,
        .pWaitSemaphores = semaphore_imageIsAvailable.Address(),
        .pWaitDstStageMask = &waitDstStage,
        .commandBufferCount = 1,
        .pCommandBuffers = commandBuffer.Address()
    };
    graphicsBase::Base().SubmitCommandBuffer_Graphics(submitInfo, fence);
    fence.WaitAndReset();
    graphicsBase::Base().PresentImage();

    graphicsBase::Plus().CommandPool_Graphics().FreeBuffers(commandBuffer);
}

我们在先前绘制三角形的主函数里,初始化窗口后调用它,并刻意让主线程睡眠1秒(以免立刻跳掉)来查看效果:

int main() {
    if (!InitializeWindow({ 1280, 720 }))
        return -1;
    easyVulkan::BootScreen(/*你指定的图片路径*/, VK_FORMAT_R8G8B8A8_UNORM);
    std::this_thread::sleep_for(std::chrono::seconds(1));//需要#include <thread>
    /*...*/
}

填写文件路径并运行程序,分别试试与窗口等大、与窗口大小不同的图像以测试不同的程序分支。
注意如果是透明图片,最终呈现到窗口后,透明度会丢失。而由stb_image读取png得到的图像数据,A通道值为0的像素点,其RGB通道未必为0,如有必要则自行对数据进行处理。