This is exciting news. I agree with you, it does look like stride settings, most likely when presenting to BView from Vulkan swap chain image. Keep in mind that the tiling format is implementation specific.
If you need a single vulkan based “Save Screenshot” function (from the swapchain image), have a look at https://github.com/SaschaWillems/Vulkan/blob/master/examples/screenshot/screenshot.cpp, the function saveScreenshot() on line 186. It can be added with almost no other dependancies. This function works if the swapchain image is created with VK_IMAGE_USAGE_TRANSFER_SRC_BIT flag.
Here is a self contained snip from my Engine using the above code.
/* PROJECT: Yarra
COPYRIGHT: 2017-2020, Zen Yes Pty Ltd, Australia
AUTHORS: Zenja Solaja
DESCRIPTION: https://github.com/SaschaWillems/Vulkan/blob/master/examples/screenshot/screenshot.cpp
Take a screenshot from the current swapchain image
This is done using a blit from the swapchain image to a linear image whose memory content is then saved as a ppm image
Getting the image date directly from a swapchain image wouldn’t work as they’re usually stored in an implementation dependent optimal tiling format
Note: This requires the swapchain images to be created with the VK_IMAGE_USAGE_TRANSFER_SRC_BIT flag (see VulkanSwapChain::create)
*/
#include
#include
#include “stb/stb_image_write.h”
#include “webp/webp/encode.h”
#include “Yarra/Render/RenderManager.h”
namespace yrender
{
static VkImageMemoryBarrier imageMemoryBarrier()
{
VkImageMemoryBarrier imageMemoryBarrier {};
imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
return imageMemoryBarrier;
}
static void insertImageMemoryBarrier(VkCommandBuffer cmdbuffer, VkImage image, VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
VkImageLayout oldImageLayout, VkImageLayout newImageLayout,
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
VkImageSubresourceRange subresourceRange)
{
VkImageMemoryBarrier _imageMemoryBarrier = imageMemoryBarrier();
_imageMemoryBarrier.srcAccessMask = srcAccessMask;
_imageMemoryBarrier.dstAccessMask = dstAccessMask;
_imageMemoryBarrier.oldLayout = oldImageLayout;
_imageMemoryBarrier.newLayout = newImageLayout;
_imageMemoryBarrier.image = image;
_imageMemoryBarrier.subresourceRange = subresourceRange;
vkCmdPipelineBarrier(
cmdbuffer,
srcStageMask,
dstStageMask,
0,
0, nullptr,
0, nullptr,
1, &_imageMemoryBarrier);
}
/* FUNCTION: RenderManager :: SaveScreenshot
ARGUMENTS: filename (no extension)
encode_format
RETURN: n/a
DESCRIPTION: Save screenshot
*/
void RenderManager :: SaveScreenshot(const char *filename, EncodeFormat encode_format)
{
#if VULKAN_ALLOW_SCREENSHOT
bool supportsBlit = true;
// Check blit support for source and destination
// Check if the device supports blitting from optimal images (the swapchain images are in optimal format)
vk::FormatProperties formatProps = fVulkanPhysicalDevice.getFormatProperties(fVulkanSwapChainFormat);
if (!(formatProps.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitSrc))
{
yplatform::Debug("[SaveScreenshot] Device does not support blitting from optimal tiled images, using copy instead of blit!\n");
supportsBlit = false;
}
// Check if the device supports blitting to linear images
formatProps = fVulkanPhysicalDevice.getFormatProperties(vk::Format::eR8G8B8A8Unorm);
if (!(formatProps.linearTilingFeatures & vk::FormatFeatureFlagBits::eBlitDst))
{
yplatform::Debug("[SaveScreenshot] Device does not support blitting to linear tiled images, using copy instead of blit!\n");
supportsBlit = false;
}
// Source for the copy is the last rendered swapchain image
vk::Image srcImage = fVulkanSwapChainImages[fCurrentFrameIndex];
// Create the linear tiled destination image to copy to and to read the memory from
vk::ImageCreateInfo imageCreateCI;
imageCreateCI.imageType = vk::ImageType::e2D;
// Note that vkCmdBlitImage (if supported) will also do format conversions if the swapchain color format would differ
imageCreateCI.format = vk::Format::eR8G8B8A8Unorm;
imageCreateCI.extent.width = fVulkanSwapChainExtent.width;
imageCreateCI.extent.height = fVulkanSwapChainExtent.height;
imageCreateCI.extent.depth = 1;
imageCreateCI.arrayLayers = 1;
imageCreateCI.mipLevels = 1;
imageCreateCI.initialLayout = vk::ImageLayout::eUndefined;
imageCreateCI.samples = vk::SampleCountFlagBits::e1;
imageCreateCI.tiling = vk::ImageTiling::eLinear;
imageCreateCI.usage = vk::ImageUsageFlagBits::eTransferDst;
// Create the image
vk::Image dstImage = fVulkanDevice.createImage(imageCreateCI, nullptr);
// Create memory to back up the image
vk::MemoryRequirements memRequirements = fVulkanDevice.getImageMemoryRequirements(dstImage);
vk::MemoryAllocateInfo memAllocInfo;
memAllocInfo.allocationSize = memRequirements.size;
// Memory must be host visible to copy from
memAllocInfo.memoryTypeIndex = FindMemoryType(memRequirements.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
vk::DeviceMemory dstImageMemory = fVulkanDevice.allocateMemory(memAllocInfo, nullptr);
fVulkanDevice.bindImageMemory(dstImage, dstImageMemory, 0);
// Do the actual blit from the swapchain image to our host visible destination image
//vk::CommandBuffer copyCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
vk::CommandBufferAllocateInfo allocInfo;
allocInfo.level = vk::CommandBufferLevel::ePrimary;
allocInfo.commandPool = fVulkanCommandPool;
allocInfo.commandBufferCount = 1;
vk::CommandBuffer copyCmd;
vk::Result res = fVulkanDevice.allocateCommandBuffers(&allocInfo, ©Cmd);
if (res != vk::Result::eSuccess)
throw std::runtime_error(vk::to_string(res));
vk::CommandBufferBeginInfo beginInfo;
copyCmd.begin(beginInfo);
// Transition destination image to transfer destination layout
insertImageMemoryBarrier(
copyCmd,
dstImage,
0,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VkImageSubresourceRange{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
// Transition swapchain image from present to transfer source layout
insertImageMemoryBarrier(
copyCmd,
srcImage,
VK_ACCESS_MEMORY_READ_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VkImageSubresourceRange{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
// If source and destination support blit we'll blit as this also does automatic format conversion (e.g. from BGR to RGB)
if (supportsBlit)
{
// Define the region to blit (we will blit the whole swapchain image)
VkOffset3D blitSize;
blitSize.x = fVulkanSwapChainExtent.width;
blitSize.y = fVulkanSwapChainExtent.height;
blitSize.z = 1;
VkImageBlit imageBlitRegion{};
imageBlitRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageBlitRegion.srcSubresource.layerCount = 1;
imageBlitRegion.srcOffsets[1] = blitSize;
imageBlitRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageBlitRegion.dstSubresource.layerCount = 1;
imageBlitRegion.dstOffsets[1] = blitSize;
// Issue the blit command
vkCmdBlitImage(
copyCmd,
srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&imageBlitRegion,
VK_FILTER_NEAREST);
}
else
{
// Otherwise use image copy (requires us to manually flip components)
VkImageCopy imageCopyRegion{};
imageCopyRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageCopyRegion.srcSubresource.layerCount = 1;
imageCopyRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageCopyRegion.dstSubresource.layerCount = 1;
imageCopyRegion.extent.width = fVulkanSwapChainExtent.width;
imageCopyRegion.extent.height = fVulkanSwapChainExtent.height;
imageCopyRegion.extent.depth = 1;
// Issue the copy command
vkCmdCopyImage(
copyCmd,
srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&imageCopyRegion);
}
// Transition destination image to general layout, which is the required layout for mapping the image memory later on
insertImageMemoryBarrier(
copyCmd,
dstImage,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_MEMORY_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_GENERAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VkImageSubresourceRange{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
// Transition back the swap chain image after the blit is done
insertImageMemoryBarrier(
copyCmd,
srcImage,
VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_MEMORY_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VkImageSubresourceRange{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
copyCmd.end();
vk::SubmitInfo submitInfo;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = ©Cmd;
vk::FenceCreateInfo fenceInfo;
vk::Fence fence = fVulkanDevice.createFence(fenceInfo, nullptr);
res = fVulkanGraphicsQueue.submit(1, &submitInfo, nullptr);
if (res != vk::Result::eSuccess)
throw std::runtime_error(vk::to_string(res));
fVulkanGraphicsQueue.waitIdle();
fVulkanDevice.destroyFence(fence);
fVulkanDevice.freeCommandBuffers(fVulkanCommandPool, 1, ©Cmd);
// Get layout of the image (including row pitch)
VkImageSubresource subResource { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0 };
VkSubresourceLayout subResourceLayout = fVulkanDevice.getImageSubresourceLayout(dstImage, subResource);
// Map image memory so we can start copying from it
const char* data;
vkMapMemory(fVulkanDevice, dstImageMemory, 0, VK_WHOLE_SIZE, 0, (void**)&data);
data += subResourceLayout.offset;
std::string name(filename);
if (encode_format == EncodeFormat::ePng)
{
name.append(".png");
stbi_write_png(name.c_str(), fVulkanSwapChainExtent.width, fVulkanSwapChainExtent.height, 4, data, (int) subResourceLayout.rowPitch);
}
else if (encode_format == EncodeFormat::eWebp)
{
name.append(".webp");
uint8_t *output;
size_t sz = WebPEncodeRGBA((const uint8_t *)data, fVulkanSwapChainExtent.width, fVulkanSwapChainExtent.height, (int) subResourceLayout.rowPitch, 100, &output);
std::ofstream file(name.c_str(), std::ios::out | std::ios::binary);
file.write((char *)output, sz);
file.close();
WebPFree(output);
}
else
{
name.append(".ppm");
std::ofstream file(name.c_str(), std::ios::out | std::ios::binary);
// ppm header
file << "P6\n" << fVulkanSwapChainExtent.width << "\n" << fVulkanSwapChainExtent.height << "\n" << 255 << "\n";
// If source is BGR (destination is always RGB) and we can't use blit (which does automatic conversion), we'll have to manually swizzle color components
bool colorSwizzle = false;
// Check if source is BGR
// Note: Not complete, only contains most common and basic BGR surface formats for demonstration purposes
if (!supportsBlit)
{
std::vector<vk::Format> formatsBGR = { vk::Format::eB8G8R8A8Srgb, vk::Format::eB8G8R8A8Unorm, vk::Format::eB8G8R8A8Snorm};
colorSwizzle = (std::find(formatsBGR.begin(), formatsBGR.end(), fVulkanSwapChainFormat) != formatsBGR.end());
}
// ppm binary pixel data
for (uint32_t y = 0; y < fVulkanSwapChainExtent.height; y++)
{
unsigned int *row = (unsigned int*)data;
for (uint32_t x = 0; x < fVulkanSwapChainExtent.width; x++)
{
if (colorSwizzle)
{
file.write((char*)row+2, 1);
file.write((char*)row+1, 1);
file.write((char*)row, 1);
}
else
{
file.write((char*)row, 3);
}
row++;
}
data += subResourceLayout.rowPitch;
}
file.close();
}
std::cout << "Screenshot saved to disk" << std::endl;
// Clean up resources
vkUnmapMemory(fVulkanDevice, dstImageMemory);
vkFreeMemory(fVulkanDevice, dstImageMemory, nullptr);
vkDestroyImage(fVulkanDevice, dstImage, nullptr);
#endif
}
}; // namespace yrender