/* * Vulkan Example - Multi threaded command buffer generation and rendering * * Copyright (C) 2016 by Sascha Willems - www.saschawillems.de * * This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) */ #include #include #include #include #include #include #include #include #define GLM_FORCE_RADIANS #define GLM_FORCE_DEPTH_ZERO_TO_ONE #include #include #include #include "vulkanexamplebase.h" #include "threadpool.hpp" #include "frustum.hpp" #include "VulkanModel.hpp" #define ENABLE_VALIDATION false //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::wstring g_ScreenshotRequest; bool g_TakingScreenshot = false; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool OnOptickStateChanged(Optick::State::Type state) { if (state == Optick::State::STOP_CAPTURE) { wchar_t tempPath[MAX_PATH] = { 0 }; GetTempPath(MAX_PATH, tempPath); std::wstring fullPath(tempPath); g_ScreenshotRequest = fullPath + L"OptickScreenshot.bmp"; g_TakingScreenshot = true; } if (state == Optick::State::DUMP_CAPTURE) { if (g_TakingScreenshot) return false; // Attach screenshot Optick::AttachFile(Optick::File::OPTICK_IMAGE, "Screenshot.bmp", g_ScreenshotRequest.c_str()); // Remove temp file _wremove(g_ScreenshotRequest.c_str()); g_ScreenshotRequest.clear(); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class VulkanExample : public VulkanExampleBase { public: bool displaySkybox = true; // Vertex layout for the models vks::VertexLayout vertexLayout = vks::VertexLayout({ vks::VERTEX_COMPONENT_POSITION, vks::VERTEX_COMPONENT_NORMAL, vks::VERTEX_COMPONENT_COLOR, }); struct { vks::Model ufo; vks::Model skysphere; } models; // Shared matrices used for thread push constant blocks struct { glm::mat4 projection; glm::mat4 view; } matrices; struct { VkPipeline phong; VkPipeline starsphere; } pipelines; VkPipelineLayout pipelineLayout; VkCommandBuffer primaryCommandBuffer; // Secondary scene command buffers used to store backgdrop and user interface struct SecondaryCommandBuffers { VkCommandBuffer background; VkCommandBuffer ui; } secondaryCommandBuffers; // Number of animated objects to be renderer // by using threads and secondary command buffers uint32_t numObjectsPerThread; // Multi threaded stuff // Max. number of concurrent threads uint32_t numThreads; // Use push constants to update shader // parameters on a per-thread base struct ThreadPushConstantBlock { glm::mat4 mvp; glm::vec3 color; }; struct ObjectData { glm::mat4 model; glm::vec3 pos; glm::vec3 rotation; float rotationDir; float rotationSpeed; float scale; float deltaT; float stateT = 0; bool visible = true; }; struct ThreadData { VkCommandPool commandPool; // One command buffer per render object std::vector commandBuffer; // One push constant block per render object std::vector pushConstBlock; // Per object information (position, rotation, etc.) std::vector objectData; }; std::vector threadData; vks::ThreadPool threadPool; // Fence to wait for all command buffers to finish before // presenting to the swap chain VkFence renderFence = {}; // Max. dimension of the ufo mesh for use as the sphere // radius for frustum culling float objectSphereDim; // View frustum for culling invisible objects vks::Frustum frustum; std::default_random_engine rndEngine; VulkanExample() : VulkanExampleBase(ENABLE_VALIDATION) { zoom = -32.5f; zoomSpeed = 2.5f; rotationSpeed = 0.5f; rotation = { 0.0f, 37.5f, 0.0f }; title = "Multi threaded command buffer"; settings.overlay = true; // Get number of max. concurrrent threads numThreads = std::thread::hardware_concurrency() / 2; assert(numThreads > 0); #if defined(__ANDROID__) LOGD("numThreads = %d", numThreads); #else std::cout << "numThreads = " << numThreads << std::endl; #endif threadPool.setThreadCount(numThreads); numObjectsPerThread = 512 / numThreads; rndEngine.seed(benchmark.active ? 0 : (unsigned)time(nullptr)); OPTICK_SET_STATE_CHANGED_CALLBACK(OnOptickStateChanged); } ~VulkanExample() { // Clean up used Vulkan resources // Note : Inherited destructor cleans up resources stored in base class vkDestroyPipeline(device, pipelines.phong, nullptr); vkDestroyPipeline(device, pipelines.starsphere, nullptr); vkDestroyPipelineLayout(device, pipelineLayout, nullptr); models.ufo.destroy(); models.skysphere.destroy(); for (auto& thread : threadData) { vkFreeCommandBuffers(device, thread.commandPool, thread.commandBuffer.size(), thread.commandBuffer.data()); vkDestroyCommandPool(device, thread.commandPool, nullptr); } vkDestroyFence(device, renderFence, nullptr); } float rnd(float range) { std::uniform_real_distribution rndDist(0.0f, range); return rndDist(rndEngine); } // Create all threads and initialize shader push constants void prepareMultiThreadedRenderer() { // Since this demo updates the command buffers on each frame // we don't use the per-framebuffer command buffers from the // base class, and create a single primary command buffer instead VkCommandBufferAllocateInfo cmdBufAllocateInfo = vks::initializers::commandBufferAllocateInfo( cmdPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1); VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, &primaryCommandBuffer)); // Create additional secondary CBs for background and ui cmdBufAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_SECONDARY; VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, &secondaryCommandBuffers.background)); VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, &secondaryCommandBuffers.ui)); threadData.resize(numThreads); float maxX = std::floor(std::sqrt(numThreads * numObjectsPerThread)); uint32_t posX = 0; uint32_t posZ = 0; for (uint32_t i = 0; i < numThreads; i++) { ThreadData *thread = &threadData[i]; // Create one command pool for each thread VkCommandPoolCreateInfo cmdPoolInfo = vks::initializers::commandPoolCreateInfo(); cmdPoolInfo.queueFamilyIndex = swapChain.queueNodeIndex; cmdPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; VK_CHECK_RESULT(vkCreateCommandPool(device, &cmdPoolInfo, nullptr, &thread->commandPool)); // One secondary command buffer per object that is updated by this thread thread->commandBuffer.resize(numObjectsPerThread); // Generate secondary command buffers for each thread VkCommandBufferAllocateInfo secondaryCmdBufAllocateInfo = vks::initializers::commandBufferAllocateInfo( thread->commandPool, VK_COMMAND_BUFFER_LEVEL_SECONDARY, thread->commandBuffer.size()); VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &secondaryCmdBufAllocateInfo, thread->commandBuffer.data())); thread->pushConstBlock.resize(numObjectsPerThread); thread->objectData.resize(numObjectsPerThread); for (uint32_t j = 0; j < numObjectsPerThread; j++) { float theta = 2.0f * float(M_PI) * rnd(1.0f); float phi = acos(1.0f - 2.0f * rnd(1.0f)); thread->objectData[j].pos = glm::vec3(sin(phi) * cos(theta), 0.0f, cos(phi)) * 35.0f; thread->objectData[j].rotation = glm::vec3(0.0f, rnd(360.0f), 0.0f); thread->objectData[j].deltaT = rnd(1.0f); thread->objectData[j].rotationDir = (rnd(100.0f) < 50.0f) ? 1.0f : -1.0f; thread->objectData[j].rotationSpeed = (2.0f + rnd(4.0f)) * thread->objectData[j].rotationDir; thread->objectData[j].scale = 0.75f + rnd(0.5f); thread->pushConstBlock[j].color = glm::vec3(rnd(1.0f), rnd(1.0f), rnd(1.0f)); } } } // Builds the secondary command buffer for each thread void threadRenderCode(uint32_t threadIndex, uint32_t cmdBufferIndex, VkCommandBufferInheritanceInfo inheritanceInfo) { OPTICK_CATEGORY("Render", Optick::Category::Rendering); ThreadData *thread = &threadData[threadIndex]; ObjectData *objectData = &thread->objectData[cmdBufferIndex]; // Check visibility against view frustum objectData->visible = frustum.checkSphere(objectData->pos, objectSphereDim * 0.5f); if (!objectData->visible) { return; } VkCommandBufferBeginInfo commandBufferBeginInfo = vks::initializers::commandBufferBeginInfo(); commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT; commandBufferBeginInfo.pInheritanceInfo = &inheritanceInfo; VkCommandBuffer cmdBuffer = thread->commandBuffer[cmdBufferIndex]; VK_CHECK_RESULT(vkBeginCommandBuffer(cmdBuffer, &commandBufferBeginInfo)); { OPTICK_GPU_CONTEXT(cmdBuffer); OPTICK_GPU_EVENT("DrawUFO"); VkViewport viewport = vks::initializers::viewport((float)width, (float)height, 0.0f, 1.0f); vkCmdSetViewport(cmdBuffer, 0, 1, &viewport); VkRect2D scissor = vks::initializers::rect2D(width, height, 0, 0); vkCmdSetScissor(cmdBuffer, 0, 1, &scissor); { OPTICK_EVENT("vkCmdBindPipeline"); vkCmdBindPipeline(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.phong); } // Update if (!paused) { OPTICK_EVENT("UpdateUFO"); objectData->rotation.y += 2.5f * objectData->rotationSpeed * frameTimer; if (objectData->rotation.y > 360.0f) { objectData->rotation.y -= 360.0f; } objectData->deltaT += 0.15f * frameTimer; if (objectData->deltaT > 1.0f) objectData->deltaT -= 1.0f; objectData->pos.y = sin(glm::radians(objectData->deltaT * 360.0f)) * 2.5f; } { OPTICK_EVENT("UpdateMVP"); objectData->model = glm::translate(glm::mat4(1.0f), objectData->pos); objectData->model = glm::rotate(objectData->model, -sinf(glm::radians(objectData->deltaT * 360.0f)) * 0.25f, glm::vec3(objectData->rotationDir, 0.0f, 0.0f)); objectData->model = glm::rotate(objectData->model, glm::radians(objectData->rotation.y), glm::vec3(0.0f, objectData->rotationDir, 0.0f)); objectData->model = glm::rotate(objectData->model, glm::radians(objectData->deltaT * 360.0f), glm::vec3(0.0f, objectData->rotationDir, 0.0f)); objectData->model = glm::scale(objectData->model, glm::vec3(objectData->scale)); thread->pushConstBlock[cmdBufferIndex].mvp = matrices.projection * matrices.view * objectData->model; } // Update shader push constant block // Contains model view matrix { OPTICK_EVENT("vkCmdPushConstants"); vkCmdPushConstants( cmdBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(ThreadPushConstantBlock), &thread->pushConstBlock[cmdBufferIndex]); } VkDeviceSize offsets[1] = { 0 }; { OPTICK_EVENT("vkCmdBindVertexBuffers"); vkCmdBindVertexBuffers(cmdBuffer, 0, 1, &models.ufo.vertices.buffer, offsets); } { OPTICK_EVENT("vkCmdBindIndexBuffer"); vkCmdBindIndexBuffer(cmdBuffer, models.ufo.indices.buffer, 0, VK_INDEX_TYPE_UINT32); } { OPTICK_EVENT("vkCmdDrawIndexed"); vkCmdDrawIndexed(cmdBuffer, models.ufo.indexCount, 1, 0, 0, 0); } } VK_CHECK_RESULT(vkEndCommandBuffer(cmdBuffer)); } void updateSecondaryCommandBuffers(VkCommandBufferInheritanceInfo inheritanceInfo) { // Secondary command buffer for the sky sphere VkCommandBufferBeginInfo commandBufferBeginInfo = vks::initializers::commandBufferBeginInfo(); commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT; commandBufferBeginInfo.pInheritanceInfo = &inheritanceInfo; VkViewport viewport = vks::initializers::viewport((float)width, (float)height, 0.0f, 1.0f); VkRect2D scissor = vks::initializers::rect2D(width, height, 0, 0); /* Background */ VK_CHECK_RESULT(vkBeginCommandBuffer(secondaryCommandBuffers.background, &commandBufferBeginInfo)); vkCmdSetViewport(secondaryCommandBuffers.background, 0, 1, &viewport); vkCmdSetScissor(secondaryCommandBuffers.background, 0, 1, &scissor); vkCmdBindPipeline(secondaryCommandBuffers.background, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.starsphere); glm::mat4 view = glm::mat4(1.0f); view = glm::rotate(view, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); view = glm::rotate(view, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); view = glm::rotate(view, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); glm::mat4 mvp = matrices.projection * view; vkCmdPushConstants( secondaryCommandBuffers.background, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(mvp), &mvp); VkDeviceSize offsets[1] = { 0 }; vkCmdBindVertexBuffers(secondaryCommandBuffers.background, 0, 1, &models.skysphere.vertices.buffer, offsets); vkCmdBindIndexBuffer(secondaryCommandBuffers.background, models.skysphere.indices.buffer, 0, VK_INDEX_TYPE_UINT32); vkCmdDrawIndexed(secondaryCommandBuffers.background, models.skysphere.indexCount, 1, 0, 0, 0); VK_CHECK_RESULT(vkEndCommandBuffer(secondaryCommandBuffers.background)); /* User interface With VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS, the primary command buffer's content has to be defined by secondary command buffers, which also applies to the UI overlay command buffer */ VK_CHECK_RESULT(vkBeginCommandBuffer(secondaryCommandBuffers.ui, &commandBufferBeginInfo)); vkCmdSetViewport(secondaryCommandBuffers.ui, 0, 1, &viewport); vkCmdSetScissor(secondaryCommandBuffers.ui, 0, 1, &scissor); vkCmdBindPipeline(secondaryCommandBuffers.ui, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.starsphere); if (settings.overlay) { drawUI(secondaryCommandBuffers.ui); } VK_CHECK_RESULT(vkEndCommandBuffer(secondaryCommandBuffers.ui)); } // Updates the secondary command buffers using a thread pool // and puts them into the primary command buffer that's // lat submitted to the queue for rendering void updateCommandBuffers(VkFramebuffer frameBuffer) { OPTICK_EVENT(); // Contains the list of secondary command buffers to be submitted std::vector commandBuffers; VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo(); VkClearValue clearValues[2]; clearValues[0].color = defaultClearColor; clearValues[1].depthStencil = { 1.0f, 0 }; VkRenderPassBeginInfo renderPassBeginInfo = vks::initializers::renderPassBeginInfo(); renderPassBeginInfo.renderPass = renderPass; renderPassBeginInfo.renderArea.offset.x = 0; renderPassBeginInfo.renderArea.offset.y = 0; renderPassBeginInfo.renderArea.extent.width = width; renderPassBeginInfo.renderArea.extent.height = height; renderPassBeginInfo.clearValueCount = 2; renderPassBeginInfo.pClearValues = clearValues; renderPassBeginInfo.framebuffer = frameBuffer; // Set target frame buffer VK_CHECK_RESULT(vkBeginCommandBuffer(primaryCommandBuffer, &cmdBufInfo)); // The primary command buffer does not contain any rendering commands // These are stored (and retrieved) from the secondary command buffers vkCmdBeginRenderPass(primaryCommandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS); // Inheritance info for the secondary command buffers VkCommandBufferInheritanceInfo inheritanceInfo = vks::initializers::commandBufferInheritanceInfo(); inheritanceInfo.renderPass = renderPass; // Secondary command buffer also use the currently active framebuffer inheritanceInfo.framebuffer = frameBuffer; // Update secondary sene command buffers updateSecondaryCommandBuffers(inheritanceInfo); if (displaySkybox) { commandBuffers.push_back(secondaryCommandBuffers.background); } // Add a job to the thread's queue for each object to be rendered for (uint32_t t = 0; t < numThreads; t++) { for (uint32_t i = 0; i < numObjectsPerThread; i++) { threadPool.threads[t]->addJob([=] { threadRenderCode(t, i, inheritanceInfo); }); } } threadPool.wait(); // Only submit if object is within the current view frustum for (uint32_t t = 0; t < numThreads; t++) { for (uint32_t i = 0; i < numObjectsPerThread; i++) { if (threadData[t].objectData[i].visible) { commandBuffers.push_back(threadData[t].commandBuffer[i]); } } } // Render ui last if (UIOverlay.visible) { commandBuffers.push_back(secondaryCommandBuffers.ui); } // Execute render commands from the secondary command buffer { OPTICK_EVENT("vkCmdExecuteCommands"); vkCmdExecuteCommands(primaryCommandBuffer, commandBuffers.size(), commandBuffers.data()); } { OPTICK_EVENT("vkCmdEndRenderPass"); vkCmdEndRenderPass(primaryCommandBuffer); } VK_CHECK_RESULT(vkEndCommandBuffer(primaryCommandBuffer)); } void loadAssets() { models.ufo.loadFromFile(getAssetPath() + "models/retroufo_red.dae", vertexLayout, 0.12f, vulkanDevice, queue); models.skysphere.loadFromFile(getAssetPath() + "models/sphere.obj", vertexLayout, 1.0f, vulkanDevice, queue); objectSphereDim = std::max(std::max(models.ufo.dim.size.x, models.ufo.dim.size.y), models.ufo.dim.size.z); } void setupPipelineLayout() { VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = vks::initializers::pipelineLayoutCreateInfo(nullptr, 0); // Push constants for model matrices VkPushConstantRange pushConstantRange = vks::initializers::pushConstantRange( VK_SHADER_STAGE_VERTEX_BIT, sizeof(ThreadPushConstantBlock), 0); // Push constant ranges are part of the pipeline layout pPipelineLayoutCreateInfo.pushConstantRangeCount = 1; pPipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange; VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pPipelineLayoutCreateInfo, nullptr, &pipelineLayout)); } void preparePipelines() { VkPipelineInputAssemblyStateCreateInfo inputAssemblyState = vks::initializers::pipelineInputAssemblyStateCreateInfo( VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0, VK_FALSE); VkPipelineRasterizationStateCreateInfo rasterizationState = vks::initializers::pipelineRasterizationStateCreateInfo( VK_POLYGON_MODE_FILL, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE, 0); VkPipelineColorBlendAttachmentState blendAttachmentState = vks::initializers::pipelineColorBlendAttachmentState( 0xf, VK_FALSE); VkPipelineColorBlendStateCreateInfo colorBlendState = vks::initializers::pipelineColorBlendStateCreateInfo( 1, &blendAttachmentState); VkPipelineDepthStencilStateCreateInfo depthStencilState = vks::initializers::pipelineDepthStencilStateCreateInfo( VK_TRUE, VK_TRUE, VK_COMPARE_OP_LESS_OR_EQUAL); VkPipelineViewportStateCreateInfo viewportState = vks::initializers::pipelineViewportStateCreateInfo(1, 1, 0); VkPipelineMultisampleStateCreateInfo multisampleState = vks::initializers::pipelineMultisampleStateCreateInfo( VK_SAMPLE_COUNT_1_BIT, 0); std::vector dynamicStateEnables = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; VkPipelineDynamicStateCreateInfo dynamicState = vks::initializers::pipelineDynamicStateCreateInfo( dynamicStateEnables.data(), dynamicStateEnables.size(), 0); std::array shaderStages; VkGraphicsPipelineCreateInfo pipelineCI = vks::initializers::pipelineCreateInfo(pipelineLayout, renderPass, 0); pipelineCI.pInputAssemblyState = &inputAssemblyState; pipelineCI.pRasterizationState = &rasterizationState; pipelineCI.pColorBlendState = &colorBlendState; pipelineCI.pMultisampleState = &multisampleState; pipelineCI.pViewportState = &viewportState; pipelineCI.pDepthStencilState = &depthStencilState; pipelineCI.pDynamicState = &dynamicState; pipelineCI.stageCount = shaderStages.size(); pipelineCI.pStages = shaderStages.data(); // Vertex bindings and attributes const std::vector vertexInputBindings = { vks::initializers::vertexInputBindingDescription(0, vertexLayout.stride(), VK_VERTEX_INPUT_RATE_VERTEX), }; const std::vector vertexInputAttributes = { vks::initializers::vertexInputAttributeDescription(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), // Location 0: Position vks::initializers::vertexInputAttributeDescription(0, 1, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3), // Location 1: Normal vks::initializers::vertexInputAttributeDescription(0, 2, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 6), // Location 2: Color }; VkPipelineVertexInputStateCreateInfo vertexInputStateCI = vks::initializers::pipelineVertexInputStateCreateInfo(); vertexInputStateCI.vertexBindingDescriptionCount = static_cast(vertexInputBindings.size()); vertexInputStateCI.pVertexBindingDescriptions = vertexInputBindings.data(); vertexInputStateCI.vertexAttributeDescriptionCount = static_cast(vertexInputAttributes.size()); vertexInputStateCI.pVertexAttributeDescriptions = vertexInputAttributes.data(); pipelineCI.pVertexInputState = &vertexInputStateCI; // Object rendering pipeline shaderStages[0] = loadShader(getAssetPath() + "shaders/multithreading/phong.vert.spv", VK_SHADER_STAGE_VERTEX_BIT); shaderStages[1] = loadShader(getAssetPath() + "shaders/multithreading/phong.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT); VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipelines.phong)); // Star sphere rendering pipeline rasterizationState.cullMode = VK_CULL_MODE_FRONT_BIT; depthStencilState.depthWriteEnable = VK_FALSE; shaderStages[0] = loadShader(getAssetPath() + "shaders/multithreading/starsphere.vert.spv", VK_SHADER_STAGE_VERTEX_BIT); shaderStages[1] = loadShader(getAssetPath() + "shaders/multithreading/starsphere.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT); VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCI, nullptr, &pipelines.starsphere)); } void updateMatrices() { matrices.projection = glm::perspective(glm::radians(60.0f), (float)width / (float)height, 0.1f, 256.0f); matrices.view = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, zoom)); matrices.view = glm::rotate(matrices.view, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); matrices.view = glm::rotate(matrices.view, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); matrices.view = glm::rotate(matrices.view, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); frustum.update(matrices.projection * matrices.view); } void draw() { OPTICK_EVENT(); // Wait for fence to signal that all command buffers are ready VkResult fenceRes; do { OPTICK_EVENT("vkWaitForFences"); fenceRes = vkWaitForFences(device, 1, &renderFence, VK_TRUE, 100000000); } while (fenceRes == VK_TIMEOUT); VK_CHECK_RESULT(fenceRes); vkResetFences(device, 1, &renderFence); VulkanExampleBase::prepareFrame(); updateCommandBuffers(frameBuffers[currentBuffer]); submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &primaryCommandBuffer; VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, renderFence)); if (g_TakingScreenshot) { saveScreenshot(g_ScreenshotRequest.c_str()); g_TakingScreenshot = false; } VulkanExampleBase::submitFrame(); } void prepare() { OPTICK_EVENT(); VulkanExampleBase::prepare(); // Create a fence for synchronization VkFenceCreateInfo fenceCreateInfo = vks::initializers::fenceCreateInfo(VK_FENCE_CREATE_SIGNALED_BIT); vkCreateFence(device, &fenceCreateInfo, nullptr, &renderFence); loadAssets(); setupPipelineLayout(); preparePipelines(); prepareMultiThreadedRenderer(); updateMatrices(); prepared = true; } virtual void render() { OPTICK_EVENT(); if (!prepared) return; draw(); } virtual void viewChanged() { updateMatrices(); } virtual void OnUpdateUIOverlay(vks::UIOverlay *overlay) { if (overlay->header("Statistics")) { overlay->text("Active threads: %d", numThreads); } if (overlay->header("Settings")) { overlay->checkBox("Skybox", &displaySkybox); } } // szPathName : Specifies the pathname -> the file path to save the image // lpBits : Specifies the bitmap bits -> the buffer (content of the) image // w : Specifies the image width // h : Specifies the image height bool saveBitmap(std::ofstream& stream, void* lpBits, int w, int h) { BITMAPINFOHEADER BMIH; // BMP header BMIH.biSize = sizeof(BITMAPINFOHEADER); BMIH.biSizeImage = w * h * 3; // Create the bitmap for this OpenGL context BMIH.biSize = sizeof(BITMAPINFOHEADER); BMIH.biWidth = w; BMIH.biHeight = h; BMIH.biPlanes = 1; BMIH.biBitCount = 24; BMIH.biCompression = BI_RGB; BMIH.biSizeImage = w * h * 3; BITMAPFILEHEADER bmfh; // Other BMP header int nBitsOffset = sizeof(BITMAPFILEHEADER) + BMIH.biSize; LONG lImageSize = BMIH.biSizeImage; LONG lFileSize = nBitsOffset + lImageSize; bmfh.bfType = 'B' + ('M' << 8); bmfh.bfOffBits = nBitsOffset; bmfh.bfSize = lFileSize; bmfh.bfReserved1 = bmfh.bfReserved2 = 0; // Write the bitmap file header // Saving the first header to file stream.write((const char*)&bmfh, sizeof(BITMAPFILEHEADER)); // And then the bitmap info header // Saving the second header to file stream.write((const char*)&BMIH, sizeof(BITMAPINFOHEADER)); // Finally, write the image data itself //-- the data represents our drawing // Saving the file content in lpBits to file stream.write((const char*)lpBits, lImageSize); stream.flush(); return true; } // 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 dependant optimal tiling format // Note: This requires the swapchain images to be created with the VK_IMAGE_USAGE_TRANSFER_SRC_BIT flag (see VulkanSwapChain::create) void saveScreenshot(const wchar_t* 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)) { std::cerr << "Device does not support blitting from optimal tiled images, using copy instead of blit!" << std::endl; 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)) { std::cerr << "Device does not support blitting to linear tiled images, using copy instead of blit!" << std::endl; 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; // Note that vkCmdBlitImage (if supported) will also do format conversions if the swapchain color format would differ 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; // 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 isBGR = false; // Check if source is BGR // Note: Not complete, only contains most common and basic BGR surface formats for demonstation purposes if (!supportsBlit) { std::vector formatsBGR = { VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_SNORM }; isBGR = (std::find(formatsBGR.begin(), formatsBGR.end(), swapChain.colorFormat) != formatsBGR.end()); } std::vector buffer; buffer.resize(width * height * 3); const char* pImg = data; for (uint32_t y = 0; y < height; y++) { const char* row = (const char*)pImg; for (uint32_t x = 0; x < width; x++) { char* out = &buffer[((height - 1 - y) * width + x) * 3]; out[0] = row[isBGR ? 0 : 2]; out[1] = row[1]; out[2] = row[isBGR ? 2 : 0]; row += 4; } pImg += subResourceLayout.rowPitch; } std::ofstream file(filename, std::ios::out | std::ios::binary); saveBitmap(file, buffer.data(), width, height); std::cout << "Screenshot saved to disk" << std::endl; // Clean up resources vkUnmapMemory(device, dstImageMemory); vkFreeMemory(device, dstImageMemory, nullptr); vkDestroyImage(device, dstImage, nullptr); } }; VULKAN_EXAMPLE_MAIN()