// Copyright 2019-2020 David Robillard // Copyright 2019 Jordan Halase // SPDX-License-Identifier: ISC /* A simple example of drawing with Vulkan. For a more advanced demo that actually draws something interesting, see pugl_vulkan_cpp_demo.cpp. */ #include "demo_utils.h" #include "test/test_utils.h" #include "pugl/pugl.h" #include "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; } 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) { puglHide(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; memset(&app, 0, sizeof(app)); VulkanState* vk = &app.vk; const PuglSpan defaultWidth = 640; const PuglSpan defaultHeight = 360; // 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"); } 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 Demo"); puglSetSizeHint(app.view, PUGL_DEFAULT_SIZE, defaultWidth, defaultHeight); 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(puglGetInstanceProcAddrFunc(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)}; puglShow(app.view); while (!app.quit) { puglUpdate(app.world, -1.0); if (app.opts.continuous) { puglPrintFps(app.world, &fpsPrinter, &app.framesDrawn); } } destroyWorld(&app); return 0; }