From 4ae4dd5b09c04246cd5cfc26572d3840e993b95a Mon Sep 17 00:00:00 2001 From: David Robillard Date: Mon, 26 Oct 2020 20:48:50 +0100 Subject: Add Vulkan demos --- examples/pugl_vulkan_demo.c | 1134 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1134 insertions(+) create mode 100644 examples/pugl_vulkan_demo.c (limited to 'examples/pugl_vulkan_demo.c') diff --git a/examples/pugl_vulkan_demo.c b/examples/pugl_vulkan_demo.c new file mode 100644 index 0000000..ef4550a --- /dev/null +++ b/examples/pugl_vulkan_demo.c @@ -0,0 +1,1134 @@ +/* + Copyright 2019-2020 David Robillard + Copyright 2019 Jordan Halase + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +/** + @file pugl_vulkan_demo.c + @brief A simple example of drawing with Vulkan. +*/ + +#include "demo_utils.h" +#include "test/test_utils.h" + +#include "pugl/pugl.h" +#include "pugl/pugl_vulkan.h" + +#include +#include + +#include +#include +#include +#include +#include + +#define CLAMP(x, l, h) ((x) <= (l) ? (l) : (x) >= (h) ? (h) : (x)) + +// Vulkan allocation callbacks which can be used for debugging +#define ALLOC_VK NULL + +// Helper macro for allocating arrays by type, with C++ compatible cast +#define AALLOC(size, Type) ((Type*)calloc(size, sizeof(Type))) + +// Helper macro for counted array arguments to make clang-format behave +#define COUNTED(count, ...) count, __VA_ARGS__ + +/// Dynamically loaded Vulkan API functions +typedef struct { + PFN_vkCreateDebugReportCallbackEXT vkCreateDebugReportCallbackEXT; + PFN_vkDestroyDebugReportCallbackEXT vkDestroyDebugReportCallbackEXT; +} InstanceAPI; + +/// Vulkan swapchain and everything that depends on it +typedef struct { + VkSwapchainKHR rawSwapchain; + uint32_t nImages; + VkExtent2D extent; + VkImage* images; + VkImageView* imageViews; + VkFence* fences; + VkCommandBuffer* commandBuffers; +} Swapchain; + +/// Synchronization semaphores +typedef struct { + VkSemaphore presentComplete; + VkSemaphore renderFinished; +} Sync; + +/// Vulkan state, purely Vulkan functions can depend on only this +typedef struct { + InstanceAPI api; + VkInstance instance; + VkDebugReportCallbackEXT debugCallback; + VkSurfaceKHR surface; + VkSurfaceFormatKHR surfaceFormat; + VkPresentModeKHR presentMode; + VkPhysicalDeviceProperties deviceProperties; + VkPhysicalDevice physicalDevice; + uint32_t graphicsIndex; + VkDevice device; + VkQueue graphicsQueue; + VkCommandPool commandPool; + Swapchain* swapchain; + Sync sync; +} VulkanState; + +/// Complete application +typedef struct { + PuglTestOptions opts; + PuglWorld* world; + PuglView* view; + VulkanState vk; + uint32_t framesDrawn; + uint32_t width; + uint32_t height; + bool quit; +} VulkanApp; + +static VKAPI_ATTR VkBool32 VKAPI_CALL +debugCallback(VkDebugReportFlagsEXT flags, + VkDebugReportObjectTypeEXT objType, + uint64_t obj, + size_t location, + int32_t code, + const char* layerPrefix, + const char* msg, + void* userData) +{ + (void)userData; + (void)objType; + (void)obj; + (void)location; + (void)code; + + if (flags & VK_DEBUG_REPORT_INFORMATION_BIT_EXT) { + fprintf(stderr, "note: "); + } else if (flags & VK_DEBUG_REPORT_WARNING_BIT_EXT) { + fprintf(stderr, "warning: "); + } else if (flags & VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT) { + fprintf(stderr, "performance warning: "); + } else if (flags & VK_DEBUG_REPORT_ERROR_BIT_EXT) { + fprintf(stderr, "error: "); + } else if (flags & VK_DEBUG_REPORT_DEBUG_BIT_EXT) { + fprintf(stderr, "debug: "); + } + + fprintf(stderr, "%s: ", layerPrefix); + fprintf(stderr, "%s\n", msg); + return VK_FALSE; +} + +static bool +hasExtension(const char* const name, + const VkExtensionProperties* const properties, + const uint32_t count) +{ + for (uint32_t i = 0; i < count; ++i) { + if (!strcmp(properties[i].extensionName, name)) { + return true; + } + } + + return false; +} + +static bool +hasLayer(const char* const name, + const VkLayerProperties* const properties, + const uint32_t count) +{ + for (uint32_t i = 0; i < count; ++i) { + if (!strcmp(properties[i].layerName, name)) { + return true; + } + } + + return false; +} + +static void +pushString(const char*** const array, + uint32_t* const count, + const char* const string) +{ + *array = (const char**)realloc(*array, (*count + 1) * sizeof(const char*)); + (*array)[*count] = string; + ++*count; +} + +static VkResult +createInstance(VulkanApp* const app) +{ + const VkApplicationInfo appInfo = { + VK_STRUCTURE_TYPE_APPLICATION_INFO, + NULL, + "Pugl Vulkan Test", + VK_MAKE_VERSION(0, 1, 0), + "Pugl Vulkan Test Engine", + VK_MAKE_VERSION(0, 1, 0), + VK_MAKE_VERSION(1, 0, 0), + }; + + // Get the number of supported extensions and layers + VkResult vr = VK_SUCCESS; + uint32_t nExtProps = 0; + uint32_t nLayerProps = 0; + if ((vr = vkEnumerateInstanceLayerProperties(&nLayerProps, NULL)) || + (vr = vkEnumerateInstanceExtensionProperties(NULL, &nExtProps, NULL))) { + return vr; + } + + // Get properties of supported extensions + VkExtensionProperties* extProps = AALLOC(nExtProps, VkExtensionProperties); + vkEnumerateInstanceExtensionProperties(NULL, &nExtProps, extProps); + + uint32_t nExtensions = 0; + const char** extensions = NULL; + + // Add extensions required by pugl + uint32_t nPuglExts = 0; + const char* const* puglExts = puglGetInstanceExtensions(&nPuglExts); + for (uint32_t i = 0; i < nPuglExts; ++i) { + pushString(&extensions, &nExtensions, puglExts[i]); + } + + // Add extra extensions we want to use if they are supported + if (hasExtension("VK_EXT_debug_report", extProps, nExtProps)) { + pushString(&extensions, &nExtensions, "VK_EXT_debug_report"); + } + + // Get properties of supported layers + VkLayerProperties* layerProps = AALLOC(nLayerProps, VkLayerProperties); + vkEnumerateInstanceLayerProperties(&nLayerProps, layerProps); + + // Add validation layers if error checking is enabled + uint32_t nLayers = 0; + const char** layers = NULL; + if (app->opts.errorChecking) { + const char* debugLayers[] = {"VK_LAYER_KHRONOS_validation", + "VK_LAYER_LUNARG_standard_validation", + NULL}; + + for (const char** l = debugLayers; *l; ++l) { + if (hasLayer(*l, layerProps, nLayerProps)) { + pushString(&layers, &nLayers, *l); + } + } + } + + for (uint32_t i = 0; i < nExtensions; ++i) { + printf("Using instance extension: %s\n", extensions[i]); + } + + for (uint32_t i = 0; i < nLayers; ++i) { + printf("Using instance layer: %s\n", layers[i]); + } + + const VkInstanceCreateInfo createInfo = { + VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + NULL, + 0, + &appInfo, + COUNTED(nLayers, layers), + COUNTED(nExtensions, extensions), + }; + + if ((vr = vkCreateInstance(&createInfo, ALLOC_VK, &app->vk.instance))) { + logError("Could not create Vulkan Instance: %d\n", vr); + } + + free(layers); + free(extensions); + free(layerProps); + free(extProps); + + return vr; +} + +static VkResult +enableDebugging(VulkanState* const vk) +{ + vk->api.vkCreateDebugReportCallbackEXT = + (PFN_vkCreateDebugReportCallbackEXT)vkGetInstanceProcAddr( + vk->instance, "vkCreateDebugReportCallbackEXT"); + + vk->api.vkDestroyDebugReportCallbackEXT = + (PFN_vkDestroyDebugReportCallbackEXT)vkGetInstanceProcAddr( + vk->instance, "vkDestroyDebugReportCallbackEXT"); + + if (vk->api.vkCreateDebugReportCallbackEXT) { + const VkDebugReportCallbackCreateInfoEXT info = { + VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT, + NULL, + VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT, + debugCallback, + NULL, + }; + + VkResult vr = VK_SUCCESS; + if ((vr = vk->api.vkCreateDebugReportCallbackEXT( + vk->instance, &info, ALLOC_VK, &vk->debugCallback))) { + logError("Could not create debug reporter: %d\n", vr); + return vr; + } + } + + return VK_SUCCESS; +} + +static VkResult +getGraphicsQueueIndex(VkSurfaceKHR surface, + VkPhysicalDevice device, + uint32_t* graphicsIndex) +{ + VkResult r = VK_SUCCESS; + + uint32_t nProps = 0; + vkGetPhysicalDeviceQueueFamilyProperties(device, &nProps, NULL); + + VkQueueFamilyProperties* props = AALLOC(nProps, VkQueueFamilyProperties); + vkGetPhysicalDeviceQueueFamilyProperties(device, &nProps, props); + + for (uint32_t q = 0; q < nProps; ++q) { + if (props[q].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + VkBool32 supported = false; + if ((r = vkGetPhysicalDeviceSurfaceSupportKHR( + device, q, surface, &supported))) { + free(props); + return r; + } else if (supported) { + *graphicsIndex = q; + free(props); + return VK_SUCCESS; + } + } + } + + free(props); + return VK_ERROR_FEATURE_NOT_PRESENT; +} + +static bool +supportsRequiredExtensions(const VkPhysicalDevice device) +{ + uint32_t nExtProps = 0; + vkEnumerateDeviceExtensionProperties(device, NULL, &nExtProps, NULL); + + VkExtensionProperties* extProps = AALLOC(nExtProps, VkExtensionProperties); + vkEnumerateDeviceExtensionProperties(device, NULL, &nExtProps, extProps); + + for (uint32_t i = 0; i < nExtProps; ++i) { + if (!strcmp(extProps[i].extensionName, + VK_KHR_SWAPCHAIN_EXTENSION_NAME)) { + free(extProps); + return true; + } + } + + free(extProps); + return false; +} + +static bool +isDeviceSuitable(const VulkanState* const vk, + const VkPhysicalDevice device, + uint32_t* const graphicsIndex) +{ + if (!supportsRequiredExtensions(device) || + getGraphicsQueueIndex(vk->surface, device, graphicsIndex)) { + return false; + } + + return true; +} + +/** + Selects a physical graphics device. + + This doesn't try to be clever, and just selects the first suitable device. +*/ +static VkResult +selectPhysicalDevice(VulkanState* const vk) +{ + VkResult vr = VK_SUCCESS; + if (!vk->surface) { + logError("Cannot select a physical device without a surface\n"); + return VK_ERROR_SURFACE_LOST_KHR; + } + + uint32_t nDevices = 0; + if ((vr = vkEnumeratePhysicalDevices(vk->instance, &nDevices, NULL))) { + logError("Failed to get count of physical devices: %d\n", vr); + return vr; + } + + if (!nDevices) { + logError("No physical devices found\n"); + return VK_ERROR_DEVICE_LOST; + } + + VkPhysicalDevice* devices = AALLOC(nDevices, VkPhysicalDevice); + if ((vr = vkEnumeratePhysicalDevices(vk->instance, &nDevices, devices))) { + logError("Failed to enumerate physical devices: %d\n", vr); + free(devices); + return vr; + } + + uint32_t i = 0; + for (i = 0; i < nDevices; ++i) { + VkPhysicalDeviceProperties deviceProps = {0}; + vkGetPhysicalDeviceProperties(devices[i], &deviceProps); + + uint32_t graphicsIndex = 0; + if (isDeviceSuitable(vk, devices[i], &graphicsIndex)) { + printf("Using device %u/%u: \"%s\"\n", + i + 1, + nDevices, + deviceProps.deviceName); + vk->deviceProperties = deviceProps; + vk->physicalDevice = devices[i]; + vk->graphicsIndex = graphicsIndex; + printf("Using graphics queue family: %u\n", vk->graphicsIndex); + break; + } + + printf("Device \"%s\" not suitable\n", deviceProps.deviceName); + } + + if (i >= nDevices) { + logError("No suitable devices found\n"); + vr = VK_ERROR_DEVICE_LOST; + } + + free(devices); + return vr; +} + +/// Opens the logical device and sets up the queue and command pool +static VkResult +openDevice(VulkanState* const vk) +{ + if (vk->device) { + logError("Renderer already has an opened device\n"); + return VK_NOT_READY; + } + + const float graphicsQueuePriority = 1.0f; + const char* const swapchainName = VK_KHR_SWAPCHAIN_EXTENSION_NAME; + + const VkDeviceQueueCreateInfo queueCreateInfo = { + VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + NULL, + 0, + vk->graphicsIndex, + COUNTED(1, &graphicsQueuePriority), + }; + + const VkDeviceCreateInfo createInfo = { + VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + NULL, + 0, + COUNTED(1, &queueCreateInfo), + COUNTED(0, NULL), + COUNTED(1, &swapchainName), + NULL, + }; + + VkDevice device = NULL; + VkResult vr = VK_SUCCESS; + if ((vr = vkCreateDevice( + vk->physicalDevice, &createInfo, ALLOC_VK, &device))) { + logError("Could not open device \"%s\": %d\n", + vk->deviceProperties.deviceName, + vr); + return vr; + } + + vk->device = device; + vkGetDeviceQueue(vk->device, vk->graphicsIndex, 0, &vk->graphicsQueue); + + const VkCommandPoolCreateInfo commandInfo = { + VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + NULL, + 0, + vk->graphicsIndex, + }; + + if ((vr = vkCreateCommandPool( + vk->device, &commandInfo, ALLOC_VK, &vk->commandPool))) { + logError("Could not create command pool: %d\n", vr); + return vr; + } + + return VK_SUCCESS; +} + +static const char* +presentModeString(const VkPresentModeKHR presentMode) +{ + switch (presentMode) { + case VK_PRESENT_MODE_IMMEDIATE_KHR: + return "Immediate"; + case VK_PRESENT_MODE_MAILBOX_KHR: + return "Mailbox"; + case VK_PRESENT_MODE_FIFO_KHR: + return "FIFO"; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: + return "FIFO relaxed"; + default: + return "Other"; + } +} + +static bool +hasPresentMode(const VkPresentModeKHR mode, + const VkPresentModeKHR* const presentModes, + const uint32_t nPresentModes) +{ + for (uint32_t i = 0; i < nPresentModes; ++i) { + if (presentModes[i] == mode) { + return true; + } + } + + return false; +} + +/// Configure the surface for the currently opened device +static VkResult +configureSurface(VulkanState* const vk) +{ + uint32_t nFormats = 0; + vkGetPhysicalDeviceSurfaceFormatsKHR(vk->physicalDevice, + vk->surface, + &nFormats, + NULL); + if (!nFormats) { + logError("No surface formats available\n"); + return VK_ERROR_FORMAT_NOT_SUPPORTED; + } + + VkSurfaceFormatKHR* surfaceFormats = AALLOC(nFormats, VkSurfaceFormatKHR); + vkGetPhysicalDeviceSurfaceFormatsKHR(vk->physicalDevice, + vk->surface, + &nFormats, + surfaceFormats); + + const VkSurfaceFormatKHR want = {VK_FORMAT_B8G8R8A8_UNORM, + VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + + uint32_t formatIndex = 0; + for (formatIndex = 0; formatIndex < nFormats; ++formatIndex) { + if (surfaceFormats[formatIndex].format == want.format && + surfaceFormats[formatIndex].colorSpace == want.colorSpace) { + vk->surfaceFormat = want; + break; + } + } + free(surfaceFormats); + if (formatIndex >= nFormats) { + logError("Could not find a suitable surface format\n"); + return VK_ERROR_FORMAT_NOT_SUPPORTED; + } + + uint32_t nPresentModes = 0; + vkGetPhysicalDeviceSurfacePresentModesKHR(vk->physicalDevice, + vk->surface, + &nPresentModes, + NULL); + if (!nPresentModes) { + logError("No present modes available\n"); + return VK_ERROR_FORMAT_NOT_SUPPORTED; + } + + VkPresentModeKHR* presentModes = AALLOC(nPresentModes, VkPresentModeKHR); + vkGetPhysicalDeviceSurfacePresentModesKHR(vk->physicalDevice, + vk->surface, + &nPresentModes, + presentModes); + + const VkPresentModeKHR tryModes[] = { + VK_PRESENT_MODE_MAILBOX_KHR, + VK_PRESENT_MODE_FIFO_RELAXED_KHR, + VK_PRESENT_MODE_FIFO_KHR, + VK_PRESENT_MODE_IMMEDIATE_KHR, + }; + + VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR; + for (uint32_t i = 0; i < sizeof(tryModes) / sizeof(VkPresentModeKHR); ++i) { + if (hasPresentMode(tryModes[i], presentModes, nPresentModes)) { + presentMode = tryModes[i]; + break; + } + } + + free(presentModes); + vk->presentMode = presentMode; + printf("Using present mode: \"%s\" (%u)\n", + presentModeString(presentMode), + presentMode); + + return VK_SUCCESS; +} + +static VkResult +createRawSwapchain(VulkanState* const vk, + const uint32_t width, + const uint32_t height) +{ + VkSurfaceCapabilitiesKHR surfaceCapabilities; + VkResult vr = VK_SUCCESS; + if ((vr = vkGetPhysicalDeviceSurfaceCapabilitiesKHR( + vk->physicalDevice, vk->surface, &surfaceCapabilities))) { + logError("Could not get surface capabilities: %d\n", vr); + return vr; + } + + /* There is a known race condition with window/surface sizes, so we clamp + to what Vulkan reports and hope for the best. */ + + vk->swapchain->extent.width = + CLAMP(width, + surfaceCapabilities.minImageExtent.width, + surfaceCapabilities.maxImageExtent.width); + + vk->swapchain->extent.height = + CLAMP(height, + surfaceCapabilities.minImageExtent.height, + surfaceCapabilities.maxImageExtent.height); + + vk->swapchain->nImages = surfaceCapabilities.minImageCount; + + const VkSwapchainCreateInfoKHR createInfo = { + VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + NULL, + 0, + vk->surface, + vk->swapchain->nImages, + vk->surfaceFormat.format, + VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, + vk->swapchain->extent, + 1, + (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT), + VK_SHARING_MODE_EXCLUSIVE, + COUNTED(0, NULL), + surfaceCapabilities.currentTransform, + VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + vk->presentMode, + VK_TRUE, + 0, + }; + + if ((vr = vkCreateSwapchainKHR(vk->device, + &createInfo, + ALLOC_VK, + &vk->swapchain->rawSwapchain))) { + logError("Could not create swapchain: %d\n", vr); + return vr; + } + + return VK_SUCCESS; +} + +static VkResult +recordCommandBuffers(VulkanState* const vk) +{ + const VkClearColorValue clearValue = {{ + 0xA4 / (float)0x100, // R + 0x1E / (float)0x100, // G + 0x22 / (float)0x100, // B + 0xFF / (float)0x100, // A + }}; + + const VkImageSubresourceRange range = { + VK_IMAGE_ASPECT_COLOR_BIT, + 0, + 1, + 0, + 1, + }; + + const VkCommandBufferBeginInfo beginInfo = { + VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + NULL, + VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT, + NULL, + }; + + for (uint32_t i = 0; i < vk->swapchain->nImages; ++i) { + const VkImageMemoryBarrier toClearBarrier = { + VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + NULL, + VK_ACCESS_MEMORY_READ_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + vk->graphicsIndex, + vk->graphicsIndex, + vk->swapchain->images[i], + range, + }; + + const VkImageMemoryBarrier toPresentBarrier = { + VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + NULL, + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_MEMORY_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + vk->graphicsIndex, + vk->graphicsIndex, + vk->swapchain->images[i], + range, + }; + + vkBeginCommandBuffer(vk->swapchain->commandBuffers[i], &beginInfo); + + vkCmdPipelineBarrier(vk->swapchain->commandBuffers[i], + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, + COUNTED(0, NULL), + COUNTED(0, NULL), + COUNTED(1, &toClearBarrier)); + + vkCmdClearColorImage(vk->swapchain->commandBuffers[i], + vk->swapchain->images[i], + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + &clearValue, + COUNTED(1, &range)); + + vkCmdPipelineBarrier(vk->swapchain->commandBuffers[i], + VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, + COUNTED(0, NULL), + COUNTED(0, NULL), + COUNTED(1, &toPresentBarrier)); + + vkEndCommandBuffer(vk->swapchain->commandBuffers[i]); + } + + return VK_SUCCESS; +} + +static VkResult +createSwapchain(VulkanState* const vk, + const uint32_t width, + const uint32_t height) +{ + VkResult vr = VK_SUCCESS; + + vk->swapchain = AALLOC(1, Swapchain); + if ((vr = createRawSwapchain(vk, width, height))) { + return vr; + } + + if ((vr = vkGetSwapchainImagesKHR(vk->device, + vk->swapchain->rawSwapchain, + &vk->swapchain->nImages, + NULL))) { + logError("Failed to query swapchain images: %d\n", vr); + return vr; + } + + vk->swapchain->images = AALLOC(vk->swapchain->nImages, VkImage); + if ((vr = vkGetSwapchainImagesKHR(vk->device, + vk->swapchain->rawSwapchain, + &vk->swapchain->nImages, + vk->swapchain->images))) { + logError("Failed to get swapchain images: %d\n", vr); + return vr; + } + + const VkCommandBufferAllocateInfo allocInfo = { + VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + NULL, + vk->commandPool, + VK_COMMAND_BUFFER_LEVEL_PRIMARY, + vk->swapchain->nImages, + }; + + vk->swapchain->commandBuffers = AALLOC(vk->swapchain->nImages, + VkCommandBuffer); + + if ((vr = vkAllocateCommandBuffers(vk->device, + &allocInfo, + vk->swapchain->commandBuffers))) { + logError("Could not allocate command buffers: %d\n", vr); + return vr; + } + + const VkFenceCreateInfo fenceInfo = { + VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + NULL, + VK_FENCE_CREATE_SIGNALED_BIT, + }; + vk->swapchain->fences = AALLOC(vk->swapchain->nImages, VkFence); + + for (uint32_t i = 0; i < vk->swapchain->nImages; ++i) { + if ((vr = vkCreateFence(vk->device, + &fenceInfo, + ALLOC_VK, + &vk->swapchain->fences[i]))) { + logError("Could not create render finished fence: %d\n", vr); + return vr; + } + } + + if ((vr = recordCommandBuffers(vk))) { + logError("Failed to record command buffers\n"); + return vr; + } + + return VK_SUCCESS; +} + +static void +destroySwapchain(VulkanState* const vk, Swapchain* const swapchain) +{ + if (!swapchain) { + return; + } + + for (uint32_t i = 0; i < swapchain->nImages; ++i) { + if (swapchain->fences[i]) { + vkDestroyFence(vk->device, swapchain->fences[i], ALLOC_VK); + } + + if (swapchain->imageViews && swapchain->imageViews[i]) { + vkDestroyImageView(vk->device, swapchain->imageViews[i], ALLOC_VK); + } + } + + free(swapchain->fences); + swapchain->fences = NULL; + free(swapchain->imageViews); + swapchain->imageViews = NULL; + + if (swapchain->images) { + free(swapchain->images); + swapchain->images = NULL; + } + + if (swapchain->commandBuffers) { + vkFreeCommandBuffers(vk->device, + vk->commandPool, + swapchain->nImages, + swapchain->commandBuffers); + free(swapchain->commandBuffers); + } + + if (swapchain->rawSwapchain) { + vkDestroySwapchainKHR(vk->device, swapchain->rawSwapchain, ALLOC_VK); + } + + free(swapchain); +} + +static VkResult +createSyncObjects(VulkanState* const vk) +{ + const VkSemaphoreCreateInfo info = { + VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + NULL, + 0, + }; + + vkCreateSemaphore(vk->device, &info, ALLOC_VK, &vk->sync.presentComplete); + vkCreateSemaphore(vk->device, &info, ALLOC_VK, &vk->sync.renderFinished); + return VK_SUCCESS; +} + +static void +destroySyncObjects(VulkanState* const vk) +{ + if (vk->sync.renderFinished) { + vkDestroySemaphore(vk->device, vk->sync.renderFinished, ALLOC_VK); + vk->sync.renderFinished = VK_NULL_HANDLE; + } + if (vk->sync.presentComplete) { + vkDestroySemaphore(vk->device, vk->sync.presentComplete, ALLOC_VK); + vk->sync.presentComplete = VK_NULL_HANDLE; + } +} + +static void +closeDevice(VulkanState* const vk) +{ + if (vk->device) { + vkDeviceWaitIdle(vk->device); + destroySyncObjects(vk); + destroySwapchain(vk, vk->swapchain); + if (vk->commandPool) { + vkDestroyCommandPool(vk->device, vk->commandPool, ALLOC_VK); + vk->commandPool = VK_NULL_HANDLE; + } + vk->graphicsQueue = VK_NULL_HANDLE; + vkDestroyDevice(vk->device, ALLOC_VK); + vk->device = VK_NULL_HANDLE; + } +} + +static void +destroyWorld(VulkanApp* const app) +{ + VulkanState* vk = &app->vk; + + if (vk) { + closeDevice(vk); + + if (app->view) { + puglHideWindow(app->view); + puglFreeView(app->view); + app->view = NULL; + } + if (vk->debugCallback && vk->api.vkDestroyDebugReportCallbackEXT) { + vk->api.vkDestroyDebugReportCallbackEXT(vk->instance, + vk->debugCallback, + ALLOC_VK); + vk->debugCallback = VK_NULL_HANDLE; + } + if (vk->surface) { + vkDestroySurfaceKHR(vk->instance, vk->surface, ALLOC_VK); + vk->surface = VK_NULL_HANDLE; + } + if (vk->instance) { + fflush(stderr); + vkDestroyInstance(vk->instance, ALLOC_VK); + vk->instance = VK_NULL_HANDLE; + } + if (app->world) { + puglFreeWorld(app->world); + app->world = NULL; + } + } +} + +static PuglStatus +onConfigure(PuglView* const view, const double width, const double height) +{ + VulkanApp* const app = (VulkanApp*)puglGetHandle(view); + + // We just record the size here and lazily resize the surface when exposed + app->width = (uint32_t)width; + app->height = (uint32_t)height; + + return PUGL_SUCCESS; +} + +static PuglStatus +recreateSwapchain(VulkanState* const vk, + const uint32_t width, + const uint32_t height) +{ + vkDeviceWaitIdle(vk->device); + destroySwapchain(vk, vk->swapchain); + + if (createSwapchain(vk, width, height)) { + logError("Failed to recreate swapchain\n"); + return PUGL_UNKNOWN_ERROR; + } + + return PUGL_SUCCESS; +} + +static PuglStatus +onExpose(PuglView* const view) +{ + VulkanApp* app = (VulkanApp*)puglGetHandle(view); + VulkanState* vk = &app->vk; + uint32_t imageIndex = 0; + VkResult result = VK_SUCCESS; + + // Recreate swapchain if the window size has changed + const Swapchain* swapchain = vk->swapchain; + if (swapchain->extent.width != app->width || + swapchain->extent.height != app->height) { + recreateSwapchain(vk, app->width, app->height); + } + + // Acquire the next image to render, rebuilding if necessary + while ((result = vkAcquireNextImageKHR(vk->device, + vk->swapchain->rawSwapchain, + UINT64_MAX, + vk->sync.presentComplete, + VK_NULL_HANDLE, + &imageIndex))) { + switch (result) { + case VK_SUCCESS: + break; + case VK_SUBOPTIMAL_KHR: + case VK_ERROR_OUT_OF_DATE_KHR: + recreateSwapchain(vk, app->width, app->height); + continue; + default: + logError("Could not acquire swapchain image: %d\n", result); + return PUGL_UNKNOWN_ERROR; + } + } + + // Wait until we can start rendering this frame + vkWaitForFences(vk->device, + COUNTED(1, &vk->swapchain->fences[imageIndex]), + VK_TRUE, + UINT64_MAX); + vkResetFences(vk->device, 1, &vk->swapchain->fences[imageIndex]); + + const VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + + // Submit command buffer to render this frame + const VkSubmitInfo submitInfo = { + VK_STRUCTURE_TYPE_SUBMIT_INFO, + NULL, + COUNTED(1, &vk->sync.presentComplete), + &waitStage, + COUNTED(1, &vk->swapchain->commandBuffers[imageIndex]), + COUNTED(1, &vk->sync.renderFinished)}; + if ((result = vkQueueSubmit(vk->graphicsQueue, + 1, + &submitInfo, + vk->swapchain->fences[imageIndex]))) { + logError("Could not submit to queue: %d\n", result); + return PUGL_FAILURE; + } + + // Present this frame + const VkPresentInfoKHR presentInfo = { + VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + NULL, + COUNTED(1, &vk->sync.renderFinished), + COUNTED(1, &vk->swapchain->rawSwapchain, &imageIndex, NULL), + }; + if ((result = vkQueuePresentKHR(vk->graphicsQueue, &presentInfo))) { + logError("Could not present image: %d\n", result); + } + + if (app->opts.continuous) { + ++app->framesDrawn; + } + + return PUGL_SUCCESS; +} + +static PuglStatus +onEvent(PuglView* const view, const PuglEvent* const e) +{ + VulkanApp* const app = (VulkanApp*)puglGetHandle(view); + + printEvent(e, "Event: ", app->opts.verbose); + + switch (e->type) { + case PUGL_EXPOSE: + return onExpose(view); + case PUGL_CONFIGURE: + return onConfigure(view, e->configure.width, e->configure.height); + case PUGL_CLOSE: + app->quit = 1; + break; + case PUGL_KEY_PRESS: + switch (e->key.key) { + case PUGL_KEY_ESCAPE: + case 'q': + app->quit = 1; + break; + } + break; + default: + break; + } + return PUGL_SUCCESS; +} + +int +main(int argc, char** argv) +{ + VulkanApp app = {0}; + VulkanState* vk = &app.vk; + const uint32_t defaultWidth = 640; + const uint32_t defaultHeight = 360; + const PuglRect frame = {0, 0, defaultWidth, defaultHeight}; + + // Parse command line options + app.opts = puglParseTestOptions(&argc, &argv); + if (app.opts.help) { + puglPrintTestUsage(argv[0], ""); + return 0; + } + + // Create world and view + if (!(app.world = puglNewWorld(PUGL_PROGRAM, PUGL_WORLD_THREADS))) { + return logError("Failed to create world\n"); + } else if (!(app.view = puglNewView(app.world))) { + puglFreeWorld(app.world); + return logError("Failed to create Pugl World and View\n"); + } + + // Create Vulkan instance + if (createInstance(&app)) { + puglFreeWorld(app.world); + return logError("Failed to create instance\n"); + } + + // Create window + puglSetWindowTitle(app.view, "Pugl Vulkan"); + puglSetFrame(app.view, frame); + puglSetHandle(app.view, &app); + puglSetBackend(app.view, puglVulkanBackend()); + puglSetViewHint(app.view, PUGL_RESIZABLE, app.opts.resizable); + puglSetEventFunc(app.view, onEvent); + const PuglStatus st = puglRealize(app.view); + if (st) { + puglFreeWorld(app.world); + puglFreeView(app.view); + return logError("Failed to create window (%s)\n", puglStrerror(st)); + } + + // Create Vulkan surface for Window + PuglVulkanLoader* loader = puglNewVulkanLoader(app.world); + if (puglCreateSurface( + loader, app.view, vk->instance, ALLOC_VK, &vk->surface)) { + return logError("Failed to create surface\n"); + } + + // Set up Vulkan + VkResult vr = VK_SUCCESS; + if ((vr = enableDebugging(vk)) || // + (vr = selectPhysicalDevice(vk)) || // + (vr = openDevice(vk)) || // + (vr = configureSurface(vk)) || // + (vr = createSwapchain(vk, defaultWidth, defaultHeight)) || // + (vr = createSyncObjects(vk))) { + destroyWorld(&app); + return logError("Failed to set up graphics (%d)\n", vr); + } + + printf("Swapchain images: %u\n", app.vk.swapchain->nImages); + + PuglFpsPrinter fpsPrinter = {puglGetTime(app.world)}; + puglShowWindow(app.view); + while (!app.quit) { + puglUpdate(app.world, -1.0); + + if (app.opts.continuous) { + puglPrintFps(app.world, &fpsPrinter, &app.framesDrawn); + } + } + + destroyWorld(&app); + return 0; +} -- cgit v1.2.1