Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion base/benchmark.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ namespace vks
uint32_t duration = 10;
std::vector<double> frameTimes;
std::string filename = "";
std::string screenshotPath = "";
int32_t screenshotFrameIndex = -1;

double runtime = 0.0;
uint32_t frameCount = 0;

void run(std::function<void()> renderFunc, VkPhysicalDeviceProperties deviceProps) {
void run(std::function<void()> renderFunc, std::function<void(const std::string&)> screenshotFunc, VkPhysicalDeviceProperties deviceProps) {
active = true;
this->deviceProps = deviceProps;
#if defined(_WIN32)
Expand Down Expand Up @@ -64,6 +66,10 @@ namespace vks
auto tDiff = std::chrono::duration<double, std::milli>(std::chrono::high_resolution_clock::now() - tStart).count();
runtime += tDiff;
frameTimes.push_back(tDiff);
// Capture screenshot at specified frame
if (screenshotFrameIndex >= 0 && frameCount == screenshotFrameIndex && !screenshotPath.empty()) {
screenshotFunc(screenshotPath);
}
frameCount++;
if (outputFrames != -1 && outputFrames == frameCount) break;
};
Expand All @@ -73,6 +79,9 @@ namespace vks
std::cout << "runtime: " << (runtime / 1000.0) << "\n";
std::cout << "frames : " << frameCount << "\n";
std::cout << "fps : " << frameCount / (runtime / 1000.0) << "\n";
if (!screenshotPath.empty() && screenshotFrameIndex >= 0) {
std::cout << "screenshot saved to: " << screenshotPath << "\n";
}
}
}

Expand Down
219 changes: 217 additions & 2 deletions base/vulkanexamplebase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ void VulkanExampleBase::renderLoop()
return;
#endif

benchmark.run([=] { render(); }, vulkanDevice->properties);
benchmark.run([=] { render(); }, [=](const std::string& filename) { saveScreenshot(filename); }, vulkanDevice->properties);
vkDeviceWaitIdle(device);
if (!benchmark.filename.empty()) {
benchmark.saveResults();
Expand Down Expand Up @@ -816,6 +816,8 @@ VulkanExampleBase::VulkanExampleBase()
commandLineParser.add("benchmarkresultfile", { "-bf", "--benchfilename" }, 1, "Set file name for benchmark results");
commandLineParser.add("benchmarkresultframes", { "-bt", "--benchframetimes" }, 0, "Save frame times to benchmark results file");
commandLineParser.add("benchmarkframes", { "-bfs", "--benchmarkframes" }, 1, "Only render the given number of frames");
commandLineParser.add("screenshot", { "-ss", "--screenshot" }, 1, "Save screenshot at specified frame (requires benchmark mode)");
commandLineParser.add("screenshotframe", { "-sf", "--screenshotframe" }, 1, "Frame index to capture screenshot (default: 0)");
#if (!(defined(VK_USE_PLATFORM_IOS_MVK) || defined(VK_USE_PLATFORM_MACOS_MVK) || defined(VK_USE_PLATFORM_METAL_EXT)))
commandLineParser.add("resourcepath", { "-rp", "--resourcepath" }, 1, "Set path for dir where assets and shaders folder is present");
#endif
Expand Down Expand Up @@ -874,6 +876,14 @@ VulkanExampleBase::VulkanExampleBase()
if (commandLineParser.isSet("benchmarkframes")) {
benchmark.outputFrames = commandLineParser.getValueAsInt("benchmarkframes", benchmark.outputFrames);
}
if (commandLineParser.isSet("screenshot")) {
benchmark.screenshotPath = commandLineParser.getValueAsString("screenshot", "");
screenshotPath = benchmark.screenshotPath;
}
if (commandLineParser.isSet("screenshotframe")) {
benchmark.screenshotFrameIndex = commandLineParser.getValueAsInt("screenshotframe", 0);
screenshotFrameIndex = benchmark.screenshotFrameIndex;
}
#if (!(defined(VK_USE_PLATFORM_IOS_MVK) || defined(VK_USE_PLATFORM_MACOS_MVK) || defined(VK_USE_PLATFORM_METAL_EXT)))
if(commandLineParser.isSet("resourcepath")) {
vks::tools::resourcePath = commandLineParser.getValueAsString("resourcepath", "");
Expand Down Expand Up @@ -1982,7 +1992,7 @@ void VulkanExampleBase::displayLinkOutputCb()
{
#if defined(VK_EXAMPLE_XCODE_GENERATED)
if (benchmark.active) {
benchmark.run([=] { render(); }, vulkanDevice->properties);
benchmark.run([=] { render(); }, [=](const std::string& filename) { saveScreenshot(filename); }, vulkanDevice->properties);
if (benchmark.filename != "") {
benchmark.saveResults();
}
Expand Down Expand Up @@ -3212,6 +3222,211 @@ void VulkanExampleBase::getEnabledFeatures() {}

void VulkanExampleBase::getEnabledExtensions() {}

void VulkanExampleBase::saveScreenshot(const std::string& filename)
{
bool supportsBlit = true;

// Check blit support for source and destination
VkFormatProperties formatProps;

// Check if the device supports blitting from optimal images (the swapchain images are in optimal format)
vkGetPhysicalDeviceFormatProperties(physicalDevice, swapChain.colorFormat, &formatProps);
if (!(formatProps.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT)) {
supportsBlit = false;
}

// Check if the device supports blitting to linear images
vkGetPhysicalDeviceFormatProperties(physicalDevice, VK_FORMAT_R8G8B8A8_UNORM, &formatProps);
if (!(formatProps.linearTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT)) {
supportsBlit = false;
}

// Source for the copy is the last rendered swapchain image
VkImage srcImage = swapChain.images[currentBuffer];

// Create the linear tiled destination image to copy to and to read the memory from
VkImageCreateInfo imageCreateCI(vks::initializers::imageCreateInfo());
imageCreateCI.imageType = VK_IMAGE_TYPE_2D;
imageCreateCI.format = VK_FORMAT_R8G8B8A8_UNORM;
imageCreateCI.extent.width = width;
imageCreateCI.extent.height = height;
imageCreateCI.extent.depth = 1;
imageCreateCI.arrayLayers = 1;
imageCreateCI.mipLevels = 1;
imageCreateCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageCreateCI.samples = VK_SAMPLE_COUNT_1_BIT;
imageCreateCI.tiling = VK_IMAGE_TILING_LINEAR;
imageCreateCI.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT;

// Create the image
VkImage dstImage;
VK_CHECK_RESULT(vkCreateImage(device, &imageCreateCI, nullptr, &dstImage));

// Create memory to back up the image
VkMemoryRequirements memRequirements;
VkMemoryAllocateInfo memAllocInfo(vks::initializers::memoryAllocateInfo());
VkDeviceMemory dstImageMemory;
vkGetImageMemoryRequirements(device, dstImage, &memRequirements);
memAllocInfo.allocationSize = memRequirements.size;
// Memory must be host visible to copy from
memAllocInfo.memoryTypeIndex = vulkanDevice->getMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAllocInfo, nullptr, &dstImageMemory));
VK_CHECK_RESULT(vkBindImageMemory(device, dstImage, dstImageMemory, 0));

// Do the actual blit from the swapchain image to our host visible destination image
VkCommandBuffer copyCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);

// Transition destination image to transfer destination layout
vks::tools::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
vks::tools::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 = width;
blitSize.y = 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 = width;
imageCopyRegion.extent.height = 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
vks::tools::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
vks::tools::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 });

vulkanDevice->flushCommandBuffer(copyCmd, queue);

// Get layout of the image (including row pitch)
VkImageSubresource subResource { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0 };
VkSubresourceLayout subResourceLayout;
vkGetImageSubresourceLayout(device, dstImage, &subResource, &subResourceLayout);

// Map image memory so we can start copying from it
const char* data;
vkMapMemory(device, dstImageMemory, 0, VK_WHOLE_SIZE, 0, (void**)&data);
data += subResourceLayout.offset;

std::ofstream file(filename, std::ios::out | std::ios::binary);

// ppm header
file << "P6\n" << width << "\n" << 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<VkFormat> formatsBGR = { VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_SNORM };
colorSwizzle = (std::find(formatsBGR.begin(), formatsBGR.end(), swapChain.colorFormat) != formatsBGR.end());
}

// ppm binary pixel data
for (uint32_t y = 0; y < height; y++)
{
unsigned int *row = (unsigned int*)data;
for (uint32_t x = 0; x < 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 " << filename << std::endl;

// Clean up resources
vkUnmapMemory(device, dstImageMemory);
vkFreeMemory(device, dstImageMemory, nullptr);
vkDestroyImage(device, dstImage, nullptr);
}

void VulkanExampleBase::windowResize()
{
if (!prepared)
Expand Down
6 changes: 6 additions & 0 deletions base/vulkanexamplebase.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ class VulkanExampleBase
float timerSpeed = 0.25f;
bool paused = false;

// Screenshot functionality
std::string screenshotPath = "";
int32_t screenshotFrameIndex = -1;

Camera camera;

std::string title = "Vulkan Example";
Expand Down Expand Up @@ -387,6 +391,8 @@ class VulkanExampleBase
virtual void setupRenderPass();
/** @brief (Virtual) Called after the physical device features have been read, can be used to set features to enable on the device */
virtual void getEnabledFeatures();
/** @brief Save screenshot of current swapchain image to file */
void saveScreenshot(const std::string& filename);
/** @brief (Virtual) Called after the physical device extensions have been read, can be used to enable extensions based on the supported extension listing*/
virtual void getEnabledExtensions();

Expand Down