diff --git a/data/shaders/renderheadless/triangle.frag b/data/shaders/renderheadless/triangle.frag new file mode 100644 index 00000000..f3e513b9 --- /dev/null +++ b/data/shaders/renderheadless/triangle.frag @@ -0,0 +1,10 @@ +#version 450 + +layout (location = 0) in vec3 inColor; + +layout (location = 0) out vec4 outFragColor; + +void main() +{ + outFragColor = vec4(inColor, 1.0); +} \ No newline at end of file diff --git a/data/shaders/renderheadless/triangle.frag.spv b/data/shaders/renderheadless/triangle.frag.spv new file mode 100644 index 00000000..685b3a78 Binary files /dev/null and b/data/shaders/renderheadless/triangle.frag.spv differ diff --git a/data/shaders/renderheadless/triangle.vert b/data/shaders/renderheadless/triangle.vert new file mode 100644 index 00000000..b89716f4 --- /dev/null +++ b/data/shaders/renderheadless/triangle.vert @@ -0,0 +1,20 @@ +#version 450 + +layout (location = 0) in vec3 inPos; +layout (location = 1) in vec3 inColor; + +layout (location = 0) out vec3 outColor; + +out gl_PerVertex { + vec4 gl_Position; +}; + +layout(push_constant) uniform PushConsts { + mat4 mvp; +} pushConsts; + +void main() +{ + outColor = inColor; + gl_Position = pushConsts.mvp * vec4(inPos.xyz, 1.0); +} diff --git a/data/shaders/renderheadless/triangle.vert.spv b/data/shaders/renderheadless/triangle.vert.spv new file mode 100644 index 00000000..e4041000 Binary files /dev/null and b/data/shaders/renderheadless/triangle.vert.spv differ diff --git a/renderheadless/renderheadless.cpp b/renderheadless/renderheadless.cpp new file mode 100644 index 00000000..966c73ac --- /dev/null +++ b/renderheadless/renderheadless.cpp @@ -0,0 +1,874 @@ +/* +* Vulkan Example - Minimal headless rendering example +* +* Copyright (C) 2017 by Sascha Willems - www.saschawillems.de +* +* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) +*/ + +#if defined(_WIN32) +#pragma comment(linker, "/subsystem:console") +#elif defined(VK_USE_PLATFORM_ANDROID_KHR) +#include +#include +#include +#include +#include "VulkanAndroid.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include + +#include +#include "VulkanTools.h" + +#if defined(VK_USE_PLATFORM_ANDROID_KHR) +android_app* androidapp; +#endif + +//#define DEBUG (!NDEBUG) +#define DEBUG true + +#define BUFFER_ELEMENTS 32 + +#if defined(VK_USE_PLATFORM_ANDROID_KHR) +#define LOG(...) ((void)__android_log_print(ANDROID_LOG_INFO, "vulkanExample", __VA_ARGS__)) +#else +#define LOG(...) printf(__VA_ARGS__) +#endif + +static VKAPI_ATTR VkBool32 VKAPI_CALL debugMessageCallback( + VkDebugReportFlagsEXT flags, + VkDebugReportObjectTypeEXT objectType, + uint64_t object, + size_t location, + int32_t messageCode, + const char* pLayerPrefix, + const char* pMessage, + void* pUserData) +{ + LOG("[VALIDATION]: %s - %s\n", pLayerPrefix, pMessage); + return VK_FALSE; +} + +class VulkanExample +{ +public: + VkInstance instance; + VkPhysicalDevice physicalDevice; + VkDevice device; + uint32_t queueFamilyIndex; + VkPipelineCache pipelineCache; + VkQueue queue; + VkCommandPool commandPool; + VkCommandBuffer commandBuffer; + VkDescriptorSetLayout descriptorSetLayout; + VkPipelineLayout pipelineLayout; + VkPipeline pipeline; + VkBuffer vertexBuffer, indexBuffer; + VkDeviceMemory vertexMemory, indexMemory; + + struct FrameBufferAttachment { + VkImage image; + VkDeviceMemory memory; + VkImageView view; + }; + int32_t width, height; + VkFramebuffer framebuffer; + FrameBufferAttachment colorAttachment, depthAttachment; + VkRenderPass renderPass; + + VkDebugReportCallbackEXT debugReportCallback; + + uint32_t getMemoryTypeIndex(uint32_t typeBits, VkMemoryPropertyFlags properties) { + VkPhysicalDeviceMemoryProperties deviceMemoryProperties; + vkGetPhysicalDeviceMemoryProperties(physicalDevice, &deviceMemoryProperties); + for (uint32_t i = 0; i < deviceMemoryProperties.memoryTypeCount; i++) { + if ((typeBits & 1) == 1) { + if ((deviceMemoryProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + typeBits >>= 1; + } + return 0; + } + + VkResult createBuffer(VkBufferUsageFlags usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, VkBuffer *buffer, VkDeviceMemory *memory, VkDeviceSize size, void *data = nullptr) + { + // Create the buffer handle + VkBufferCreateInfo bufferCreateInfo = vks::initializers::bufferCreateInfo(usageFlags, size); + bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VK_CHECK_RESULT(vkCreateBuffer(device, &bufferCreateInfo, nullptr, buffer)); + + // Create the memory backing up the buffer handle + VkMemoryRequirements memReqs; + VkMemoryAllocateInfo memAlloc = vks::initializers::memoryAllocateInfo(); + vkGetBufferMemoryRequirements(device, *buffer, &memReqs); + memAlloc.allocationSize = memReqs.size; + memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, memoryPropertyFlags); + VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, memory)); + + if (data != nullptr) { + void *mapped; + VK_CHECK_RESULT(vkMapMemory(device, *memory, 0, size, 0, &mapped)); + memcpy(mapped, data, size); + vkUnmapMemory(device, *memory); + } + + VK_CHECK_RESULT(vkBindBufferMemory(device, *buffer, *memory, 0)); + + return VK_SUCCESS; + } + + /* + Submit command buffer to a queue and wait for fence until queue operations have been finished + */ + void submitWork(VkCommandBuffer cmdBuffer, VkQueue queue) + { + VkSubmitInfo submitInfo = vks::initializers::submitInfo(); + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cmdBuffer; + VkFenceCreateInfo fenceInfo = vks::initializers::fenceCreateInfo(); + VkFence fence; + VK_CHECK_RESULT(vkCreateFence(device, &fenceInfo, nullptr, &fence)); + VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, fence)); + VK_CHECK_RESULT(vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX)); + vkDestroyFence(device, fence, nullptr); + } + + VulkanExample() + { + LOG("Running headless rendering example\n"); + +#if defined(VK_USE_PLATFORM_ANDROID_KHR) + LOG("loading vulkan lib"); + vks::android::loadVulkanLibrary(); +#endif + + VkApplicationInfo appInfo = {}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Vulkan headless example"; + appInfo.pEngineName = "VulkanExample"; + appInfo.apiVersion = VK_API_VERSION_1_0; + + /* + Vulkan instance creation (without surface extensions) + */ + VkInstanceCreateInfo instanceCreateInfo = {}; + instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + instanceCreateInfo.pApplicationInfo = &appInfo; + + uint32_t layerCount = 0; +#if defined(VK_USE_PLATFORM_ANDROID_KHR) + const char* validationlayers[] = { "VK_LAYER_GOOGLE_threading", "VK_LAYER_LUNARG_parameter_validation", "VK_LAYER_LUNARG_object_tracker","VK_LAYER_LUNARG_core_validation", "VK_LAYER_LUNARG_swapchain", "VK_LAYER_GOOGLE_unique_objects" }; + layerCount = 6; +#else + const char* validationlayers[] = { "VK_LAYER_LUNARG_standard_validation" }; + layerCount = 1; +#endif +#if DEBUG + instanceCreateInfo.ppEnabledLayerNames = validationlayers; + const char* validationExt = VK_EXT_DEBUG_REPORT_EXTENSION_NAME; + instanceCreateInfo.enabledLayerCount = layerCount; + instanceCreateInfo.enabledExtensionCount = 1; + instanceCreateInfo.ppEnabledExtensionNames = &validationExt; +#endif + VK_CHECK_RESULT(vkCreateInstance(&instanceCreateInfo, nullptr, &instance)); + +#if defined(VK_USE_PLATFORM_ANDROID_KHR) + vks::android::loadVulkanFunctions(instance); +#endif +#if DEBUG + VkDebugReportCallbackCreateInfoEXT debugReportCreateInfo = {}; + debugReportCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT; + debugReportCreateInfo.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT; + debugReportCreateInfo.pfnCallback = (PFN_vkDebugReportCallbackEXT)debugMessageCallback; + + // We have to explicitly load this function. + PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT")); + assert(vkCreateDebugReportCallbackEXT); + VK_CHECK_RESULT(vkCreateDebugReportCallbackEXT(instance, &debugReportCreateInfo, nullptr, &debugReportCallback)); +#endif + + /* + Vulkan device creation + */ + uint32_t deviceCount = 0; + VK_CHECK_RESULT(vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr)); + std::vector physicalDevices(deviceCount); + VK_CHECK_RESULT(vkEnumeratePhysicalDevices(instance, &deviceCount, physicalDevices.data())); + physicalDevice = physicalDevices[0]; + + VkPhysicalDeviceProperties deviceProperties; + vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties); + LOG("GPU: %s\n", deviceProperties.deviceName); + + // Request a single graphics queue + const float defaultQueuePriority(0.0f); + VkDeviceQueueCreateInfo queueCreateInfo = {}; + uint32_t queueFamilyCount; + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, nullptr); + std::vector queueFamilyProperties(queueFamilyCount); + vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilyProperties.data()); + for (uint32_t i = 0; i < static_cast(queueFamilyProperties.size()); i++) { + if (queueFamilyProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + queueFamilyIndex = i; + queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueCreateInfo.queueFamilyIndex = i; + queueCreateInfo.queueCount = 1; + queueCreateInfo.pQueuePriorities = &defaultQueuePriority; + break; + } + } + // Create logical device + VkDeviceCreateInfo deviceCreateInfo = {}; + deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + deviceCreateInfo.queueCreateInfoCount = 1; + deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo; + VK_CHECK_RESULT(vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device)); + + // Get a compute queue + vkGetDeviceQueue(device, queueFamilyIndex, 0, &queue); + + // Command pool + VkCommandPoolCreateInfo cmdPoolInfo = {}; + cmdPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + cmdPoolInfo.queueFamilyIndex = queueFamilyIndex; + cmdPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + VK_CHECK_RESULT(vkCreateCommandPool(device, &cmdPoolInfo, nullptr, &commandPool)); + + /* + Prepare vertex and index buffers + */ + struct Vertex { + float position[3]; + float color[3]; + }; + { + std::vector vertices = { + { { 1.0f, 1.0f, 0.0f }, { 1.0f, 0.0f, 0.0f } }, + { { -1.0f, 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } }, + { { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, 1.0f } } + }; + std::vector indices = { 0, 1, 2 }; + + const VkDeviceSize vertexBufferSize = vertices.size() * sizeof(Vertex); + const VkDeviceSize indexBufferSize = indices.size() * sizeof(uint32_t); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingMemory; + + // Command buffer for copy commands (reused) + VkCommandBufferAllocateInfo cmdBufAllocateInfo = vks::initializers::commandBufferAllocateInfo(commandPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1); + VkCommandBuffer copyCmd; + VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, ©Cmd)); + VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo(); + + // Copy input data to VRAM using a staging buffer + { + // Vertices + createBuffer( + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + &stagingBuffer, + &stagingMemory, + vertexBufferSize, + vertices.data()); + + createBuffer( + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + &vertexBuffer, + &vertexMemory, + vertexBufferSize); + + VK_CHECK_RESULT(vkBeginCommandBuffer(copyCmd, &cmdBufInfo)); + VkBufferCopy copyRegion = {}; + copyRegion.size = vertexBufferSize; + vkCmdCopyBuffer(copyCmd, stagingBuffer, vertexBuffer, 1, ©Region); + VK_CHECK_RESULT(vkEndCommandBuffer(copyCmd)); + + submitWork(copyCmd, queue); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingMemory, nullptr); + + // Indices + createBuffer( + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + &stagingBuffer, + &stagingMemory, + indexBufferSize, + indices.data()); + + createBuffer( + VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + &indexBuffer, + &indexMemory, + indexBufferSize); + + VK_CHECK_RESULT(vkBeginCommandBuffer(copyCmd, &cmdBufInfo)); + copyRegion.size = vertexBufferSize; + vkCmdCopyBuffer(copyCmd, stagingBuffer, indexBuffer, 1, ©Region); + VK_CHECK_RESULT(vkEndCommandBuffer(copyCmd)); + + submitWork(copyCmd, queue); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingMemory, nullptr); + } + } + + /* + Create framebuffer attachments + */ + width = 1024; + height = 1024; + VkFormat colorFormat = VK_FORMAT_R8G8B8A8_UNORM; + VkFormat depthFormat; + vks::tools::getSupportedDepthFormat(physicalDevice, &depthFormat); + { + // Color attachment + VkImageCreateInfo image = vks::initializers::imageCreateInfo(); + image.imageType = VK_IMAGE_TYPE_2D; + image.format = colorFormat; + image.extent.width = width; + image.extent.height = height; + image.extent.depth = 1; + image.mipLevels = 1; + image.arrayLayers = 1; + image.samples = VK_SAMPLE_COUNT_1_BIT; + image.tiling = VK_IMAGE_TILING_OPTIMAL; + image.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + + VkMemoryAllocateInfo memAlloc = vks::initializers::memoryAllocateInfo(); + VkMemoryRequirements memReqs; + + VK_CHECK_RESULT(vkCreateImage(device, &image, nullptr, &colorAttachment.image)); + vkGetImageMemoryRequirements(device, colorAttachment.image, &memReqs); + memAlloc.allocationSize = memReqs.size; + memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &colorAttachment.memory)); + VK_CHECK_RESULT(vkBindImageMemory(device, colorAttachment.image, colorAttachment.memory, 0)); + + VkImageViewCreateInfo colorImageView = vks::initializers::imageViewCreateInfo(); + colorImageView.viewType = VK_IMAGE_VIEW_TYPE_2D; + colorImageView.format = colorFormat; + colorImageView.subresourceRange = {}; + colorImageView.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + colorImageView.subresourceRange.baseMipLevel = 0; + colorImageView.subresourceRange.levelCount = 1; + colorImageView.subresourceRange.baseArrayLayer = 0; + colorImageView.subresourceRange.layerCount = 1; + colorImageView.image = colorAttachment.image; + VK_CHECK_RESULT(vkCreateImageView(device, &colorImageView, nullptr, &colorAttachment.view)); + + // Depth stencil attachment + image.format = depthFormat; + image.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + + VK_CHECK_RESULT(vkCreateImage(device, &image, nullptr, &depthAttachment.image)); + vkGetImageMemoryRequirements(device, depthAttachment.image, &memReqs); + memAlloc.allocationSize = memReqs.size; + memAlloc.memoryTypeIndex = getMemoryTypeIndex(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK_RESULT(vkAllocateMemory(device, &memAlloc, nullptr, &depthAttachment.memory)); + VK_CHECK_RESULT(vkBindImageMemory(device, depthAttachment.image, depthAttachment.memory, 0)); + + VkImageViewCreateInfo depthStencilView = vks::initializers::imageViewCreateInfo(); + depthStencilView.viewType = VK_IMAGE_VIEW_TYPE_2D; + depthStencilView.format = depthFormat; + depthStencilView.flags = 0; + depthStencilView.subresourceRange = {}; + depthStencilView.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + depthStencilView.subresourceRange.baseMipLevel = 0; + depthStencilView.subresourceRange.levelCount = 1; + depthStencilView.subresourceRange.baseArrayLayer = 0; + depthStencilView.subresourceRange.layerCount = 1; + depthStencilView.image = depthAttachment.image; + VK_CHECK_RESULT(vkCreateImageView(device, &depthStencilView, nullptr, &depthAttachment.view)); + } + + /* + Create renderpass + */ + { + std::array attchmentDescriptions = {}; + // Color attachment + attchmentDescriptions[0].format = colorFormat; + attchmentDescriptions[0].samples = VK_SAMPLE_COUNT_1_BIT; + attchmentDescriptions[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attchmentDescriptions[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attchmentDescriptions[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attchmentDescriptions[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attchmentDescriptions[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attchmentDescriptions[0].finalLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + // Depth attachment + attchmentDescriptions[1].format = depthFormat; + attchmentDescriptions[1].samples = VK_SAMPLE_COUNT_1_BIT; + attchmentDescriptions[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attchmentDescriptions[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attchmentDescriptions[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attchmentDescriptions[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attchmentDescriptions[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attchmentDescriptions[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + + VkAttachmentReference colorReference = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; + VkAttachmentReference depthReference = { 1, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL }; + + VkSubpassDescription subpassDescription = {}; + subpassDescription.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpassDescription.colorAttachmentCount = 1; + subpassDescription.pColorAttachments = &colorReference; + subpassDescription.pDepthStencilAttachment = &depthReference; + + // Use subpass dependencies for layout transitions + std::array dependencies; + + dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL; + dependencies[0].dstSubpass = 0; + dependencies[0].srcStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + dependencies[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependencies[0].srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; + dependencies[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dependencies[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; + + dependencies[1].srcSubpass = 0; + dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL; + dependencies[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependencies[1].dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + dependencies[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dependencies[1].dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; + dependencies[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; + + // Create the actual renderpass + VkRenderPassCreateInfo renderPassInfo = {}; + renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + renderPassInfo.attachmentCount = static_cast(attchmentDescriptions.size()); + renderPassInfo.pAttachments = attchmentDescriptions.data(); + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpassDescription; + renderPassInfo.dependencyCount = static_cast(dependencies.size()); + renderPassInfo.pDependencies = dependencies.data(); + VK_CHECK_RESULT(vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass)); + + VkImageView attachments[2]; + attachments[0] = colorAttachment.view; + attachments[1] = depthAttachment.view; + + VkFramebufferCreateInfo framebufferCreateInfo = vks::initializers::framebufferCreateInfo(); + framebufferCreateInfo.renderPass = renderPass; + framebufferCreateInfo.attachmentCount = 2; + framebufferCreateInfo.pAttachments = attachments; + framebufferCreateInfo.width = width; + framebufferCreateInfo.height = height; + framebufferCreateInfo.layers = 1; + VK_CHECK_RESULT(vkCreateFramebuffer(device, &framebufferCreateInfo, nullptr, &framebuffer)); + } + + /* + Prepare graphics pipeline + */ + { + std::vector setLayoutBindings = {}; + VkDescriptorSetLayoutCreateInfo descriptorLayout = + vks::initializers::descriptorSetLayoutCreateInfo(setLayoutBindings); + VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device, &descriptorLayout, nullptr, &descriptorSetLayout)); + + VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = + vks::initializers::pipelineLayoutCreateInfo(nullptr, 0); + + // MVP via push constant block + VkPushConstantRange pushConstantRange = vks::initializers::pushConstantRange(VK_SHADER_STAGE_VERTEX_BIT, sizeof(glm::mat4), 0); + pipelineLayoutCreateInfo.pushConstantRangeCount = 1; + pipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange; + + VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr, &pipelineLayout)); + + VkPipelineCacheCreateInfo pipelineCacheCreateInfo = {}; + pipelineCacheCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO; + VK_CHECK_RESULT(vkCreatePipelineCache(device, &pipelineCacheCreateInfo, nullptr, &pipelineCache)); + + // Create pipeline + 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); + + 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); + + VkPipelineMultisampleStateCreateInfo multisampleState = + vks::initializers::pipelineMultisampleStateCreateInfo(VK_SAMPLE_COUNT_1_BIT); + + std::vector dynamicStateEnables = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + VkPipelineDynamicStateCreateInfo dynamicState = + vks::initializers::pipelineDynamicStateCreateInfo(dynamicStateEnables); + + VkGraphicsPipelineCreateInfo pipelineCreateInfo = + vks::initializers::pipelineCreateInfo(pipelineLayout, renderPass); + + std::array shaderStages{}; + + pipelineCreateInfo.pInputAssemblyState = &inputAssemblyState; + pipelineCreateInfo.pRasterizationState = &rasterizationState; + pipelineCreateInfo.pColorBlendState = &colorBlendState; + pipelineCreateInfo.pMultisampleState = &multisampleState; + pipelineCreateInfo.pViewportState = &viewportState; + pipelineCreateInfo.pDepthStencilState = &depthStencilState; + pipelineCreateInfo.pDynamicState = &dynamicState; + pipelineCreateInfo.stageCount = static_cast(shaderStages.size()); + pipelineCreateInfo.pStages = shaderStages.data(); + + // Vertex bindings an attributes + // Binding description + std::vector vertexInputBindings = { + vks::initializers::vertexInputBindingDescription(0, sizeof(Vertex), VK_VERTEX_INPUT_RATE_VERTEX), + }; + + // Attribute descriptions + std::vector vertexInputAttributes = { + vks::initializers::vertexInputAttributeDescription(0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), // Position + vks::initializers::vertexInputAttributeDescription(0, 1, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3), // Color + }; + + VkPipelineVertexInputStateCreateInfo vertexInputState = vks::initializers::pipelineVertexInputStateCreateInfo(); + vertexInputState.vertexBindingDescriptionCount = static_cast(vertexInputBindings.size()); + vertexInputState.pVertexBindingDescriptions = vertexInputBindings.data(); + vertexInputState.vertexAttributeDescriptionCount = static_cast(vertexInputAttributes.size()); + vertexInputState.pVertexAttributeDescriptions = vertexInputAttributes.data(); + + pipelineCreateInfo.pVertexInputState = &vertexInputState; + + shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + shaderStages[0].pName = "main"; + shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + shaderStages[1].pName = "main"; +#if defined(VK_USE_PLATFORM_ANDROID_KHR) + shaderStages[0].module = vks::tools::loadShader(androidapp->activity->assetManager, ASSET_PATH "shaders/renderheadless/triangle.vert.spv", device); + shaderStages[1].module = vks::tools::loadShader(androidapp->activity->assetManager, ASSET_PATH "shaders/renderheadless/triangle.frag.spv", device); +#else + shaderStages[0].module = vks::tools::loadShader(ASSET_PATH "shaders/renderheadless/triangle.vert.spv", device); + shaderStages[1].module = vks::tools::loadShader(ASSET_PATH "shaders/renderheadless/triangle.frag.spv", device); +#endif + VK_CHECK_RESULT(vkCreateGraphicsPipelines(device, pipelineCache, 1, &pipelineCreateInfo, nullptr, &pipeline)); + } + + /* + Command buffer creation (for compute work submission) + */ + { + VkCommandBuffer commandBuffer; + VkCommandBufferAllocateInfo cmdBufAllocateInfo = + vks::initializers::commandBufferAllocateInfo(commandPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1); + VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, &commandBuffer)); + + VkCommandBufferBeginInfo cmdBufInfo = + vks::initializers::commandBufferBeginInfo(); + + VK_CHECK_RESULT(vkBeginCommandBuffer(commandBuffer, &cmdBufInfo)); + + VkClearValue clearValues[2]; + clearValues[0].color = { { 0.0f, 0.0f, 0.2f, 1.0f } }; + clearValues[1].depthStencil = { 1.0f, 0 }; + + VkRenderPassBeginInfo renderPassBeginInfo = {}; + renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + renderPassBeginInfo.renderArea.extent.width = width; + renderPassBeginInfo.renderArea.extent.height = height; + renderPassBeginInfo.clearValueCount = 2; + renderPassBeginInfo.pClearValues = clearValues; + renderPassBeginInfo.renderPass = renderPass; + renderPassBeginInfo.framebuffer = framebuffer; + + vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); + + VkViewport viewport = {}; + viewport.height = (float)height; + viewport.width = (float)width; + viewport.minDepth = (float)0.0f; + viewport.maxDepth = (float)1.0f; + vkCmdSetViewport(commandBuffer, 0, 1, &viewport); + + // Update dynamic scissor state + VkRect2D scissor = {}; + scissor.extent.width = width; + scissor.extent.height = height; + vkCmdSetScissor(commandBuffer, 0, 1, &scissor); + + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + + // Render scene + VkDeviceSize offsets[1] = { 0 }; + vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertexBuffer, offsets); + vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32); + + std::vector pos = { + glm::vec3(-1.5f, 0.0f, -4.0f), + glm::vec3( 0.0f, 0.0f, -2.5f), + glm::vec3( 1.5f, 0.0f, -4.0f), + }; + + for (auto v : pos) { + glm::mat4 mvpMatrix = glm::perspective(glm::radians(60.0f), (float)width / (float)height, 0.1f, 256.0f) * glm::translate(glm::mat4(), v); + vkCmdPushConstants(commandBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(mvpMatrix), &mvpMatrix); + vkCmdDrawIndexed(commandBuffer, 3, 1, 0, 0, 0); + } + + vkCmdEndRenderPass(commandBuffer); + + VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer)); + + submitWork(commandBuffer, queue); + + vkDeviceWaitIdle(device); + } + + /* + Copy framebuffer image to host visible image + */ + const char* imagedata; + { + // Create the linear tiled destination image to copy to and to read the memory from + VkImageCreateInfo imgCreateInfo(vks::initializers::imageCreateInfo()); + imgCreateInfo.imageType = VK_IMAGE_TYPE_2D; + imgCreateInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imgCreateInfo.extent.width = width; + imgCreateInfo.extent.height = height; + imgCreateInfo.extent.depth = 1; + imgCreateInfo.arrayLayers = 1; + imgCreateInfo.mipLevels = 1; + imgCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imgCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imgCreateInfo.tiling = VK_IMAGE_TILING_LINEAR; + imgCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + // Create the image + VkImage dstImage; + VK_CHECK_RESULT(vkCreateImage(device, &imgCreateInfo, 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 = getMemoryTypeIndex(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 + VkCommandBufferAllocateInfo cmdBufAllocateInfo = vks::initializers::commandBufferAllocateInfo(commandPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1); + VkCommandBuffer copyCmd; + VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, ©Cmd)); + VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo(); + VK_CHECK_RESULT(vkBeginCommandBuffer(copyCmd, &cmdBufInfo)); + + VkImageMemoryBarrier imageMemoryBarrier = vks::initializers::imageMemoryBarrier(); + + // 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 color attachment to transfer source + vks::tools::insertImageMemoryBarrier( + copyCmd, + colorAttachment.image, + 0, + VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_UNDEFINED, + 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 }); + + 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; + + vkCmdCopyImage( + copyCmd, + colorAttachment.image, 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 }); + + VK_CHECK_RESULT(vkEndCommandBuffer(copyCmd)); + + submitWork(copyCmd, queue); + + // Get layout of the image (including row pitch) + VkImageSubresource subResource{}; + subResource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + VkSubresourceLayout subResourceLayout; + + vkGetImageSubresourceLayout(device, dstImage, &subResource, &subResourceLayout); + + // Map image memory so we can start copying from it + vkMapMemory(device, dstImageMemory, 0, VK_WHOLE_SIZE, 0, (void**)&imagedata); + imagedata += subResourceLayout.offset; + + /* + Save host visible framebuffer image to disk (ppm format) + */ + +#if defined (VK_USE_PLATFORM_ANDROID_KHR) + const char* filename = strcat(getenv("EXTERNAL_STORAGE"), "/headless.ppm"); +#else + const char* filename = "headless.ppm"; +#endif + 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 and needs swizzle + std::vector formatsBGR = { VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_SNORM }; + colorSwizzle = (std::find(formatsBGR.begin(), formatsBGR.end(), VK_FORMAT_R8G8B8A8_UNORM) != formatsBGR.end()); + + // ppm binary pixel data + for (int32_t y = 0; y < height; y++) { + unsigned int *row = (unsigned int*)imagedata; + for (int32_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++; + } + imagedata += subResourceLayout.rowPitch; + } + file.close(); + + LOG("Framebuffer image saved to %s\n", filename); + + // Clean up resources + vkUnmapMemory(device, dstImageMemory); + vkFreeMemory(device, dstImageMemory, nullptr); + vkDestroyImage(device, dstImage, nullptr); + } + + vkQueueWaitIdle(queue); + +#if DEBUG + PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallback = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT")); + assert(vkDestroyDebugReportCallback); + vkDestroyDebugReportCallback(instance, debugReportCallback, nullptr); +#endif + } + + ~VulkanExample() + { + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexMemory, nullptr); + vkDestroyBuffer(device, indexBuffer, nullptr); + vkFreeMemory(device, indexMemory, nullptr); + + vkDestroyImageView(device, colorAttachment.view, nullptr); + vkDestroyImage(device, colorAttachment.image, nullptr); + vkFreeMemory(device, colorAttachment.memory, nullptr); + vkDestroyImageView(device, depthAttachment.view, nullptr); + vkDestroyImage(device, depthAttachment.image, nullptr); + vkFreeMemory(device, depthAttachment.memory, nullptr); + vkDestroyRenderPass(device, renderPass, nullptr); + vkDestroyFramebuffer(device, framebuffer, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + vkDestroyPipeline(device, pipeline, nullptr); + vkDestroyCommandPool(device, commandPool, nullptr); +#if defined(VK_USE_PLATFORM_ANDROID_KHR) + vks::android::freeVulkanLibrary(); +#endif + } +}; + +#if defined(VK_USE_PLATFORM_ANDROID_KHR) +void handleAppCommand(android_app * app, int32_t cmd) { + if (cmd == APP_CMD_INIT_WINDOW) { + VulkanExample *vulkanExample = new VulkanExample(); + delete(vulkanExample); + ANativeActivity_finish(app->activity); + } +} +void android_main(android_app* state) { + app_dummy(); + androidapp = state; + androidapp->onAppCmd = handleAppCommand; + int ident, events; + struct android_poll_source* source; + while ((ident = ALooper_pollAll(-1, NULL, &events, (void**)&source)) >= 0) { + if (source != NULL) { + source->process(androidapp, source); + } + if (androidapp->destroyRequested != 0) { + break; + } + } +} +#else +int main() { + VulkanExample *vulkanExample = new VulkanExample(); + std::cout << "Finished. Press enter to terminate..."; + getchar(); + delete(vulkanExample); + return 0; +} +#endif \ No newline at end of file diff --git a/renderheadless/renderheadless.vcxproj b/renderheadless/renderheadless.vcxproj new file mode 100644 index 00000000..e826a292 --- /dev/null +++ b/renderheadless/renderheadless.vcxproj @@ -0,0 +1,95 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {8CBD2720-82A5-480A-BB8C-3A77948AEF67} + Win32Proj + 8.1 + + + + Application + true + v140 + + + Application + false + v140 + + + + + + + + + + + + + true + $(SolutionDir)\bin\ + $(SolutionDir)\bin\intermediate\$(ProjectName)\$(ConfigurationName) + + + true + $(SolutionDir)\bin\ + $(SolutionDir)\bin\intermediate\$(ProjectName)\$(ConfigurationName) + + + + WIN32;_DEBUG;_WINDOWS;VK_USE_PLATFORM_WIN32_KHR;_USE_MATH_DEFINES;NOMINMAX;%(PreprocessorDefinitions) + MultiThreadedDebugDLL + Level3 + ProgramDatabase + Disabled + ..\base;..\external\glm;..\external\gli;..\external\assimp;..\external;%(AdditionalIncludeDirectories) + /FS %(AdditionalOptions) + + + true + Windows + ..\libs\vulkan\vulkan-1.lib;..\libs\assimp\assimp.lib;%(AdditionalDependencies) + + + + + WIN32;NDEBUG;_WINDOWS;VK_USE_PLATFORM_WIN32_KHR;_USE_MATH_DEFINES;NOMINMAX;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + MultiThreadedDLL + Level3 + ProgramDatabase + ..\base;..\external\glm;..\external\gli;..\external\assimp;..\external;%(AdditionalIncludeDirectories) + + + true + Windows + true + true + ..\libs\vulkan\vulkan-1.lib;..\libs\assimp\assimp.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/renderheadless/renderheadless.vcxproj.filters b/renderheadless/renderheadless.vcxproj.filters new file mode 100644 index 00000000..95a58be6 --- /dev/null +++ b/renderheadless/renderheadless.vcxproj.filters @@ -0,0 +1,41 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {b92c560f-2fe8-4d06-a2c8-deede9ebe4e6} + + + + + Source Files + + + Source Files + + + + + Header Files + + + + + Shaders + + + Shaders + + + \ No newline at end of file diff --git a/vulkanExamples.sln b/vulkanExamples.sln index d0fc5219..2de23c96 100644 --- a/vulkanExamples.sln +++ b/vulkanExamples.sln @@ -158,6 +158,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "stencilbuffer", "stencilbuf EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "computeheadless", "computeheadless\computeheadless.vcxproj", "{13862534-67C8-46B7-9574-531E987D8F37}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "renderheadless", "renderheadless\renderheadless.vcxproj", "{8CBD2720-82A5-480A-BB8C-3A77948AEF67}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -388,6 +390,10 @@ Global {13862534-67C8-46B7-9574-531E987D8F37}.Debug|x64.Build.0 = Debug|x64 {13862534-67C8-46B7-9574-531E987D8F37}.Release|x64.ActiveCfg = Release|x64 {13862534-67C8-46B7-9574-531E987D8F37}.Release|x64.Build.0 = Release|x64 + {8CBD2720-82A5-480A-BB8C-3A77948AEF67}.Debug|x64.ActiveCfg = Debug|x64 + {8CBD2720-82A5-480A-BB8C-3A77948AEF67}.Debug|x64.Build.0 = Debug|x64 + {8CBD2720-82A5-480A-BB8C-3A77948AEF67}.Release|x64.ActiveCfg = Release|x64 + {8CBD2720-82A5-480A-BB8C-3A77948AEF67}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE