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命令都是传输命令,所以waitDstStage是VK_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) {} };
在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_from和imb_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类型的数组,用于指定要被拷贝的数据块 |
- VkBufferImageCopy也适用于从图像将数据拷贝到缓冲区的vkCmdCopyImageToBuffer(...)命令。
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 |
图层总数 |
-
VkImageSubresourceLayers相比VkImageSubresourceRange少一个levelCount成员,关于VkImageSubresourceLayers的解说请参考Ch3-2中创建图像视图时涉及的关于VkImageSubresourceRange的解说。
于是补全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, ®ion); 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, ®ion, 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.bufferOffset、region_copy.bufferRowLength、region_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_BIT和VK_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_BIT和VK_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,如有必要则自行对数据进行处理。