/* 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; }