#include <vulkan/vulkan.h>

#include <iostream>
#include <vector>
#include <limits>
#include <cstring>
#include <cmath>

#include <SFML/Window.hpp>
#include <SFML/Graphics.hpp> // Include graphics because we use sf::Image for loading images

#include "vulkan-utils-new.hpp"

using namespace std;

namespace {

   #ifdef NDEBUG
      const bool ENABLE_VALIDATION_LAYERS = false;
   #else
      const bool ENABLE_VALIDATION_LAYERS = true;
   #endif

   typedef float Vec3[3];
   typedef float Matrix[4][4];

   // Multiply 2 matrices
   void matrixMultiply(Matrix& result, const Matrix& left, const Matrix& right) {
      Matrix temp;

      for (int i = 0; i < 4; i++)
      {
         for (int j = 0; j < 4; j++)
            temp[i][j] = left[0][j] * right[i][0] + left[1][j] * right[i][1] + left[2][j] * right[i][2] + left[3][j] * right[i][3];
      }

      std::memcpy(result, temp, sizeof(Matrix));
   }

   // Rotate a matrix around the x-axis
   void matrixRotateX(Matrix& result, float angle) {
      Matrix matrix = {
          {1.f,   0.f,             0.f,             0.f},
          {0.f,   std::cos(angle), std::sin(angle), 0.f},
          {0.f,  -std::sin(angle), std::cos(angle), 0.f},
          {0.f,   0.f,             0.f,             1.f}
      };

      matrixMultiply(result, result, matrix);
   }

   // Rotate a matrix around the y-axis
   void matrixRotateY(Matrix& result, float angle) {
      Matrix matrix = {
          { std::cos(angle), 0.f, std::sin(angle), 0.f},
          { 0.f,             1.f, 0.f,             0.f},
          {-std::sin(angle), 0.f, std::cos(angle), 0.f},
          { 0.f,             0.f, 0.f,             1.f}
      };

      matrixMultiply(result, result, matrix);
   }

   // Rotate a matrix around the z-axis
   void matrixRotateZ(Matrix& result, float angle) {
      Matrix matrix = {
          { std::cos(angle), std::sin(angle), 0.f, 0.f},
          {-std::sin(angle), std::cos(angle), 0.f, 0.f},
          { 0.f,             0.f,             1.f, 0.f},
          { 0.f,             0.f,             0.f, 1.f}
      };

      matrixMultiply(result, result, matrix);
   }

   // Construct a lookat view matrix
   void matrixLookAt(Matrix& result, const Vec3& eye, const Vec3& center, const Vec3& up) {
      // Forward-looking vector
      Vec3 forward = {
          center[0] - eye[0],
          center[1] - eye[1],
          center[2] - eye[2]
      };

      // Normalize
      float factor = 1.0f / std::sqrt(forward[0] * forward[0] + forward[1] * forward[1] + forward[2] * forward[2]);

      for (int i = 0; i < 3; i++) {
         forward[i] = forward[i] * factor;
      }

      // Side vector (Forward cross product Up)
      Vec3 side = {
          forward[1] * up[2] - forward[2] * up[1],
          forward[2] * up[0] - forward[0] * up[2],
          forward[0] * up[1] - forward[1] * up[0]
      };

      // Normalize
      factor = 1.0f / std::sqrt(side[0] * side[0] + side[1] * side[1] + side[2] * side[2]);

      for (int i = 0; i < 3; i++)
         side[i] = side[i] * factor;

      result[0][0] = side[0];
      result[0][1] = side[1] * forward[2] - side[2] * forward[1];
      result[0][2] = -forward[0];
      result[0][3] = 0.f;

      result[1][0] = side[1];
      result[1][1] = side[2] * forward[0] - side[0] * forward[2];
      result[1][2] = -forward[1];
      result[1][3] = 0.f;

      result[2][0] = side[2];
      result[2][1] = side[0] * forward[1] - side[1] * forward[0];
      result[2][2] = -forward[2];
      result[2][3] = 0.f;

      result[3][0] = (-eye[0]) * result[0][0] + (-eye[1]) * result[1][0] + (-eye[2]) * result[2][0];
      result[3][1] = (-eye[0]) * result[0][1] + (-eye[1]) * result[1][1] + (-eye[2]) * result[2][1];
      result[3][2] = (-eye[0]) * result[0][2] + (-eye[1]) * result[1][2] + (-eye[2]) * result[2][2];
      result[3][3] = (-eye[0]) * result[0][3] + (-eye[1]) * result[1][3] + (-eye[2]) * result[2][3] + 1.0f;
   }

   // Construct a perspective projection matrix
   void matrixPerspective(Matrix& result, float fov, float aspect, float nearPlane, float farPlane) {
      const float a = 1.f / std::tan(fov / 2.f);

      result[0][0] = a / aspect;
      result[0][1] = 0.f;
      result[0][2] = 0.f;
      result[0][3] = 0.f;

      result[1][0] = 0.f;
      result[1][1] = -a;
      result[1][2] = 0.f;
      result[1][3] = 0.f;

      result[2][0] = 0.f;
      result[2][1] = 0.f;
      result[2][2] = -((farPlane + nearPlane) / (farPlane - nearPlane));
      result[2][3] = -1.f;

      result[3][0] = 0.f;
      result[3][1] = 0.f;
      result[3][2] = -((2.f * farPlane * nearPlane) / (farPlane - nearPlane));
      result[3][3] = 0.f;
   }

   // Clamp a value between low and high values
   template<typename T>
   T clamp(T value, T low, T high) {
      return (value <= low) ? low : ((value >= high) ? high : value);
   }

   VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(
         VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
         VkDebugUtilsMessageTypeFlagsEXT messageType,
         const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
         void* pUserData) {
      cerr << "validation layer: " << pCallbackData->pMessage << endl;

      return VK_FALSE;
   }
}


class VulkanExample {
public:
   // Constructor
   VulkanExample() :
      window(sf::VideoMode(800, 600), "SFML window with Vulkan", sf::Style::Default),
      vulkanAvailable(sf::Vulkan::isAvailable()),
      maxFramesInFlight(2),
      currentFrame(0),
      swapchainOutOfDate(false),
      instance(0),
      surface(0),
      gpu(0),
      queueFamilyIndex(-1),
      device(0),
      queue(0),
      swapchainFormat(),
      swapchainExtent(),
      swapchain(0),
      depthFormat(VK_FORMAT_UNDEFINED),
      depthImage(0),
      depthImageMemory(0),
      depthImageView(0),
      vertexShaderModule(0),
      fragmentShaderModule(0),
      descriptorSetLayout(0),
      pipelineLayout(0),
      renderPass(0),
      graphicsPipeline(0),
      commandPool(0),
      vertexBuffer(0),
      vertexBufferMemory(0),
      indexBuffer(0),
      indexBufferMemory(0),
      textureImage(0),
      textureImageMemory(0),
      textureImageView(0),
      textureSampler(0),
      descriptorPool(0)
   {
      const vector<const char*> validationLayers = {
         "VK_LAYER_KHRONOS_validation",
         "VK_LAYER_LUNARG_monitor"                    // This should show the FPS in the title bar
      };

      // Vulkan setup procedure
      if (vulkanAvailable) setupInstance(validationLayers);
      if (vulkanAvailable) setupDebugMessenger();
      if (vulkanAvailable) setupSurface();
      if (vulkanAvailable) setupPhysicalDevice();
      if (vulkanAvailable) setupLogicalDevice();
      if (vulkanAvailable) setupSwapchain();
      if (vulkanAvailable) setupSwapchainImages();
      if (vulkanAvailable) setupShaders();
      if (vulkanAvailable) setupRenderpass();
      if (vulkanAvailable) setupDescriptorSetLayout();
      if (vulkanAvailable) setupPipelineLayout();
      if (vulkanAvailable) setupPipeline();
      if (vulkanAvailable) setupCommandPool();
      if (vulkanAvailable) setupVertexBuffer();
      if (vulkanAvailable) setupIndexBuffer();
      if (vulkanAvailable) setupUniformBuffers();
      if (vulkanAvailable) setupDepthImage();
      if (vulkanAvailable) setupDepthImageView();
      if (vulkanAvailable) setupTextureImage();
      if (vulkanAvailable) setupTextureImageView();
      if (vulkanAvailable) setupTextureSampler();
      if (vulkanAvailable) setupFramebuffers();
      if (vulkanAvailable) setupDescriptorPool();
      if (vulkanAvailable) setupDescriptorSets();
      if (vulkanAvailable) setupCommandBuffers();
      if (vulkanAvailable) setupDraw();
      if (vulkanAvailable) setupSemaphores();
      if (vulkanAvailable) setupFences();

      // If something went wrong, notify the user by setting the window title
      if (!vulkanAvailable)
         window.setTitle("SFML window with Vulkan (Vulkan not available)");
   }


   // Destructor
   ~VulkanExample() {
      // Wait until there are no pending frames
      if (device) {
         vkDeviceWaitIdle(device);
      }

      // Teardown swapchain
      cleanupSwapchain();

      // Vulkan teardown procedure
      for (std::size_t i = 0; i < fences.size(); i++)
         vkDestroyFence(device, fences[i], 0);

      for (std::size_t i = 0; i < renderFinishedSemaphores.size(); i++)
         vkDestroySemaphore(device, renderFinishedSemaphores[i], 0);

      for (std::size_t i = 0; i < imageAvailableSemaphores.size(); i++)
         vkDestroySemaphore(device, imageAvailableSemaphores[i], 0);

      if (descriptorPool)
         vkDestroyDescriptorPool(device, descriptorPool, 0);

      for (std::size_t i = 0; i < uniformBuffersMemory.size(); i++)
         vkFreeMemory(device, uniformBuffersMemory[i], 0);

      for (std::size_t i = 0; i < uniformBuffers.size(); i++)
         vkDestroyBuffer(device, uniformBuffers[i], 0);

      if (textureSampler)
         vkDestroySampler(device, textureSampler, 0);

      if (textureImageView)
         vkDestroyImageView(device, textureImageView, 0);

      if (textureImageMemory)
         vkFreeMemory(device, textureImageMemory, 0);

      if (textureImage)
         vkDestroyImage(device, textureImage, 0);

      if (indexBufferMemory)
         vkFreeMemory(device, indexBufferMemory, 0);

      if (indexBuffer)
         vkDestroyBuffer(device, indexBuffer, 0);

      if (vertexBufferMemory)
         vkFreeMemory(device, vertexBufferMemory, 0);

      if (vertexBuffer)
         vkDestroyBuffer(device, vertexBuffer, 0);

      if (commandPool)
         vkDestroyCommandPool(device, commandPool, 0);

      if (descriptorSetLayout)
         vkDestroyDescriptorSetLayout(device, descriptorSetLayout, 0);

      if (fragmentShaderModule)
         vkDestroyShaderModule(device, fragmentShaderModule, 0);

      if (vertexShaderModule)
         vkDestroyShaderModule(device, vertexShaderModule, 0);

      if (device)
         vkDestroyDevice(device, 0);

      if (surface)
         vkDestroySurfaceKHR(instance, surface, 0);

      if (ENABLE_VALIDATION_LAYERS) {
         VulkanUtilsNew::destroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
      }

      if (instance)
         vkDestroyInstance(instance, 0);
   }

   // Cleanup swapchain
   void cleanupSwapchain() {
      // Swapchain teardown procedure
      for (std::size_t i = 0; i < fences.size(); i++)
         vkWaitForFences(device, 1, &fences[i], VK_TRUE, std::numeric_limits<uint64_t>::max());

      if (commandBuffers.size())
         vkFreeCommandBuffers(device, commandPool, commandBuffers.size(), &commandBuffers[0]);

      commandBuffers.clear();

      for (std::size_t i = 0; i < swapchainFramebuffers.size(); i++)
         vkDestroyFramebuffer(device, swapchainFramebuffers[i], 0);

      swapchainFramebuffers.clear();

      if (graphicsPipeline) {
         vkDestroyPipeline(device, graphicsPipeline, 0);
      }

      if (renderPass)
         vkDestroyRenderPass(device, renderPass, 0);

      if (pipelineLayout)
         vkDestroyPipelineLayout(device, pipelineLayout, 0);

      if (depthImageView)
         vkDestroyImageView(device, depthImageView, 0);

      if (depthImageMemory)
         vkFreeMemory(device, depthImageMemory, 0);

      if (depthImage)
         vkDestroyImage(device, depthImage, 0);

      for (std::size_t i = 0; i < swapchainImageViews.size(); i++)
         vkDestroyImageView(device, swapchainImageViews[i], 0);

      swapchainImageViews.clear();

      if (swapchain) { 
         vkDestroySwapchainKHR(device, swapchain, 0);
      }
   }

   // Cleanup and recreate swapchain
   void recreateSwapchain() {
      cout << "Recreating pipeline..." << endl;

      // Wait until there are no pending frames
      vkDeviceWaitIdle(device);

      // Cleanup swapchain
      cleanupSwapchain();

      // Swapchain setup procedure
      if (vulkanAvailable) setupSwapchain();
      if (vulkanAvailable) setupSwapchainImages();
      if (vulkanAvailable) setupPipelineLayout();
      if (vulkanAvailable) setupRenderpass();
      if (vulkanAvailable) setupPipeline();
      if (vulkanAvailable) setupDepthImage();
      if (vulkanAvailable) setupDepthImageView();
      if (vulkanAvailable) setupFramebuffers();
      if (vulkanAvailable) setupCommandBuffers();
      if (vulkanAvailable) setupDraw();
   }

   // Setup Vulkan instance
   void setupInstance(const vector<const char*>& validationLayers) {
      if (ENABLE_VALIDATION_LAYERS && !VulkanUtilsNew::checkValidationLayerSupport(validationLayers)) {
         throw runtime_error("validation layers requested, but not available!");
      }

      // Retrieve the extensions we need to enable in order to use Vulkan with SFML
      std::vector<const char*> requiredExtentions = sf::Vulkan::getGraphicsRequiredInstanceExtensions();
      if (ENABLE_VALIDATION_LAYERS) {
         requiredExtentions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
      }

      // Register our application information
      VkApplicationInfo applicationInfo = VkApplicationInfo();
      applicationInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
      applicationInfo.pApplicationName = "SFML Vulkan Test";
      applicationInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
      applicationInfo.pEngineName = "SFML Vulkan Test Engine";
      applicationInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
      applicationInfo.apiVersion = VK_API_VERSION_1_0;

      VkInstanceCreateInfo instanceCreateInfo = VkInstanceCreateInfo();
      instanceCreateInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
      instanceCreateInfo.pApplicationInfo = &applicationInfo;

      instanceCreateInfo.enabledExtensionCount = requiredExtentions.size();
      instanceCreateInfo.ppEnabledExtensionNames = requiredExtentions.data();

      VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo;
      if (ENABLE_VALIDATION_LAYERS) {
         instanceCreateInfo.enabledLayerCount = validationLayers.size();
         instanceCreateInfo.ppEnabledLayerNames = validationLayers.data();

         populateDebugMessengerCreateInfo(debugCreateInfo);
         instanceCreateInfo.pNext = &debugCreateInfo;
      } else {
         instanceCreateInfo.enabledLayerCount = 0;

         instanceCreateInfo.pNext = nullptr;
      }

      if (vkCreateInstance(&instanceCreateInfo, nullptr, &instance) != VK_SUCCESS) {
         throw runtime_error("failed to create instance!");
      }
   }

   void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
      createInfo = {};
      createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
      // createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
      createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
      createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
      createInfo.pfnUserCallback = debugCallback;
   }

   // Setup our debug callback function to be called by Vulkan
   void setupDebugMessenger() {
      if (!ENABLE_VALIDATION_LAYERS) return;

      VkDebugUtilsMessengerCreateInfoEXT createInfo;
      populateDebugMessengerCreateInfo(createInfo);

      if (VulkanUtilsNew::createDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
         throw runtime_error("failed to set up debug messenger!");
      }
   }

   // Setup the SFML window Vulkan rendering surface
   void setupSurface()
   {
      if (!window.createVulkanSurface(instance, surface))
         vulkanAvailable = false;
   }

   // Select a GPU to use and query its capabilities
   void setupPhysicalDevice()
   {
      // Last sanity check
      if (!vkEnumeratePhysicalDevices || !vkCreateDevice || !vkGetPhysicalDeviceProperties)
      {
         vulkanAvailable = false;
         return;
      }

      // Retrieve list of GPUs
      uint32_t objectCount = 0;

      std::vector<VkPhysicalDevice> devices;

      if (vkEnumeratePhysicalDevices(instance, &objectCount, 0) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }

      devices.resize(objectCount);

      if (vkEnumeratePhysicalDevices(instance, &objectCount, &devices[0]) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }

      // Look for a GPU that supports swapchains
      for (std::size_t i = 0; i < devices.size(); i++)
      {
         VkPhysicalDeviceProperties deviceProperties;
         vkGetPhysicalDeviceProperties(devices[i], &deviceProperties);

         std::vector<VkExtensionProperties> extensions;

         if (vkEnumerateDeviceExtensionProperties(devices[i], 0, &objectCount, 0) != VK_SUCCESS)
         {
            vulkanAvailable = false;
            return;
         }

         extensions.resize(objectCount);

         if (vkEnumerateDeviceExtensionProperties(devices[i], 0, &objectCount, &extensions[0]) != VK_SUCCESS)
         {
            vulkanAvailable = false;
            return;
         }

         bool supportsSwapchain = false;

         for (std::size_t j = 0; j < extensions.size(); j++)
         {
            if (!std::strcmp(extensions[j].extensionName, VK_KHR_SWAPCHAIN_EXTENSION_NAME))
            {
               supportsSwapchain = true;
               break;
            }
         }

         if (!supportsSwapchain)
            continue;

         // Prefer discrete over integrated GPUs if multiple are available
         if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
         {
            gpu = devices[i];
            break;
         }
         else if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU)
         {
            gpu = devices[i];
         }
      }

      if (!gpu)
      {
         vulkanAvailable = false;
         return;
      }

      // Check what depth formats are available and select one
      VkFormatProperties formatProperties = VkFormatProperties();

      vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_D24_UNORM_S8_UINT, &formatProperties);

      if (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
         depthFormat = VK_FORMAT_D24_UNORM_S8_UINT;
      }
      else
      {
         vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_D32_SFLOAT_S8_UINT, &formatProperties);

         if (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
            depthFormat = VK_FORMAT_D32_SFLOAT_S8_UINT;
         }
         else
         {
            vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_D32_SFLOAT, &formatProperties);

            if (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
               depthFormat = VK_FORMAT_D32_SFLOAT;
            }
            else
            {
               vulkanAvailable = false;
               return;
            }
         }
      }
   }

   // Setup logical device and device queue
   void setupLogicalDevice()
   {
      // Select a queue family that supports graphics operations and surface presentation
      uint32_t objectCount = 0;

      std::vector<VkQueueFamilyProperties> queueFamilyProperties;

      vkGetPhysicalDeviceQueueFamilyProperties(gpu, &objectCount, 0);

      queueFamilyProperties.resize(objectCount);

      vkGetPhysicalDeviceQueueFamilyProperties(gpu, &objectCount, &queueFamilyProperties[0]);

      for (std::size_t i = 0; i < queueFamilyProperties.size(); i++)
      {
         VkBool32 surfaceSupported = VK_FALSE;

         vkGetPhysicalDeviceSurfaceSupportKHR(gpu, i, surface, &surfaceSupported);

         if ((queueFamilyProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && (surfaceSupported == VK_TRUE))
         {
            queueFamilyIndex = i;
            break;
         }
      }

      if (queueFamilyIndex < 0)
      {
         vulkanAvailable = false;
         return;
      }

      float queuePriority = 1.0f;

      VkDeviceQueueCreateInfo deviceQueueCreateInfo = VkDeviceQueueCreateInfo();
      deviceQueueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
      deviceQueueCreateInfo.queueCount = 1;
      deviceQueueCreateInfo.queueFamilyIndex = queueFamilyIndex;
      deviceQueueCreateInfo.pQueuePriorities = &queuePriority;

      // Enable the swapchain extension
      const char* extentions[1] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };

      // Enable anisotropic filtering
      VkPhysicalDeviceFeatures physicalDeviceFeatures = VkPhysicalDeviceFeatures();
      physicalDeviceFeatures.samplerAnisotropy = VK_TRUE;

      VkDeviceCreateInfo deviceCreateInfo = VkDeviceCreateInfo();
      deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
      deviceCreateInfo.enabledExtensionCount = 1;
      deviceCreateInfo.ppEnabledExtensionNames = extentions;
      deviceCreateInfo.queueCreateInfoCount = 1;
      deviceCreateInfo.pQueueCreateInfos = &deviceQueueCreateInfo;
      deviceCreateInfo.pEnabledFeatures = &physicalDeviceFeatures;

      // Create our logical device
      if (vkCreateDevice(gpu, &deviceCreateInfo, 0, &device) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }

      // Retrieve a handle to the logical device command queue
      vkGetDeviceQueue(device, queueFamilyIndex, 0, &queue);
   }

   // Query surface formats and set up swapchain
   void setupSwapchain() {
      cout << "STARTED CALL" << endl;
      // Select a surface format that supports RGBA color format
      uint32_t objectCount = 0;

      std::vector<VkSurfaceFormatKHR> surfaceFormats;

      if (vkGetPhysicalDeviceSurfaceFormatsKHR(gpu, surface, &objectCount, 0) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }

      surfaceFormats.resize(objectCount);

      if (vkGetPhysicalDeviceSurfaceFormatsKHR(gpu, surface, &objectCount, &surfaceFormats[0]) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }

      if ((surfaceFormats.size() == 1) && (surfaceFormats[0].format == VK_FORMAT_UNDEFINED)) {
         swapchainFormat.format = VK_FORMAT_B8G8R8A8_UNORM;
         swapchainFormat.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
      } else if (!surfaceFormats.empty()) {
         for (std::size_t i = 0; i < surfaceFormats.size(); i++) {
            if ((surfaceFormats[i].format == VK_FORMAT_B8G8R8A8_UNORM) && (surfaceFormats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)) {
               swapchainFormat.format = VK_FORMAT_B8G8R8A8_UNORM;
               swapchainFormat.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;

               break;
            }
         }

         if (swapchainFormat.format == VK_FORMAT_UNDEFINED) {
            swapchainFormat = surfaceFormats[0];
         }
      } else {
         vulkanAvailable = false;
         return;
      }

      // Select a swapchain present mode
      std::vector<VkPresentModeKHR> presentModes;

      if (vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &objectCount, 0) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }

      presentModes.resize(objectCount);

      if (vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &objectCount, &presentModes[0]) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }

      // Prefer mailbox over FIFO if it is available
      VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR;

      for (std::size_t i = 0; i < presentModes.size(); i++) {
         if (presentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR) {
            presentMode = presentModes[i];
            break;
         }
      }

      // Determine size and count of swapchain images
      VkSurfaceCapabilitiesKHR surfaceCapabilities;

      if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(gpu, surface, &surfaceCapabilities) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }

      swapchainExtent.width = clamp<uint32_t>(window.getSize().x, surfaceCapabilities.minImageExtent.width, surfaceCapabilities.maxImageExtent.width);
      swapchainExtent.height = clamp<uint32_t>(window.getSize().y, surfaceCapabilities.minImageExtent.height, surfaceCapabilities.maxImageExtent.height);

      uint32_t imageCount = clamp<uint32_t>(2, surfaceCapabilities.minImageCount, surfaceCapabilities.maxImageCount);

      VkSwapchainCreateInfoKHR swapchainCreateInfo = VkSwapchainCreateInfoKHR();
      swapchainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
      swapchainCreateInfo.surface = surface;
      swapchainCreateInfo.minImageCount = imageCount;
      swapchainCreateInfo.imageFormat = swapchainFormat.format;
      swapchainCreateInfo.imageColorSpace = swapchainFormat.colorSpace;
      swapchainCreateInfo.imageExtent = swapchainExtent;
      swapchainCreateInfo.imageArrayLayers = 1;
      swapchainCreateInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
      swapchainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
      swapchainCreateInfo.preTransform = surfaceCapabilities.currentTransform;
      swapchainCreateInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
      swapchainCreateInfo.presentMode = presentMode;
      swapchainCreateInfo.clipped = VK_TRUE;
      swapchainCreateInfo.oldSwapchain = VK_NULL_HANDLE;

      // Create the swapchain
      if (vkCreateSwapchainKHR(device, &swapchainCreateInfo, 0, &swapchain) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }
   }

   // Retrieve the swapchain images and create image views for them
   void setupSwapchainImages()
   {
      // Retrieve swapchain images
      uint32_t objectCount = 0;

      if (vkGetSwapchainImagesKHR(device, swapchain, &objectCount, 0) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }

      swapchainImages.resize(objectCount);
      swapchainImageViews.resize(objectCount);

      if (vkGetSwapchainImagesKHR(device, swapchain, &objectCount, &swapchainImages[0]) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }

      VkImageViewCreateInfo imageViewCreateInfo = VkImageViewCreateInfo();
      imageViewCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
      imageViewCreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
      imageViewCreateInfo.format = swapchainFormat.format;
      imageViewCreateInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
      imageViewCreateInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
      imageViewCreateInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
      imageViewCreateInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
      imageViewCreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
      imageViewCreateInfo.subresourceRange.baseMipLevel = 0;
      imageViewCreateInfo.subresourceRange.levelCount = 1;
      imageViewCreateInfo.subresourceRange.baseArrayLayer = 0;
      imageViewCreateInfo.subresourceRange.layerCount = 1;

      // Create an image view for each swapchain image
      for (std::size_t i = 0; i < swapchainImages.size(); i++)
      {
         imageViewCreateInfo.image = swapchainImages[i];

         if (vkCreateImageView(device, &imageViewCreateInfo, 0, &swapchainImageViews[i]) != VK_SUCCESS)
         {
            vulkanAvailable = false;
            return;
         }
      }
   }

   // Load vertex and fragment shader modules
   void setupShaders()
   {
      VkShaderModuleCreateInfo shaderModuleCreateInfo = VkShaderModuleCreateInfo();
      shaderModuleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;

      // Use the vertex shader SPIR-V code to create a vertex shader module
      {
         sf::FileInputStream file;

         if (!file.open("resources/shader.vert.spv"))
         {
            vulkanAvailable = false;
            return;
         }

         std::vector<char> buffer(static_cast<std::size_t>(file.getSize()));

         if (file.read(&buffer[0], file.getSize()) != file.getSize())
         {
            vulkanAvailable = false;
            return;
         }

         shaderModuleCreateInfo.codeSize = buffer.size();
         shaderModuleCreateInfo.pCode = reinterpret_cast<const uint32_t*>(&buffer[0]);

         if (vkCreateShaderModule(device, &shaderModuleCreateInfo, 0, &vertexShaderModule) != VK_SUCCESS)
         {
            vulkanAvailable = false;
            return;
         }
      }

      // Use the fragment shader SPIR-V code to create a fragment shader module
      {
         sf::FileInputStream file;

         if (!file.open("resources/shader.frag.spv"))
         {
            vulkanAvailable = false;
            return;
         }

         std::vector<char> buffer(static_cast<std::size_t>(file.getSize()));

         if (file.read(&buffer[0], file.getSize()) != file.getSize())
         {
            vulkanAvailable = false;
            return;
         }

         shaderModuleCreateInfo.codeSize = buffer.size();
         shaderModuleCreateInfo.pCode = reinterpret_cast<const uint32_t*>(&buffer[0]);

         if (vkCreateShaderModule(device, &shaderModuleCreateInfo, 0, &fragmentShaderModule) != VK_SUCCESS)
         {
            vulkanAvailable = false;
            return;
         }
      }

      // Prepare the shader stage information for later pipeline creation
      shaderStages[0] = VkPipelineShaderStageCreateInfo();
      shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
      shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
      shaderStages[0].module = vertexShaderModule;
      shaderStages[0].pName = "main";

      shaderStages[1] = VkPipelineShaderStageCreateInfo();
      shaderStages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
      shaderStages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
      shaderStages[1].module = fragmentShaderModule;
      shaderStages[1].pName = "main";
   }

   // Setup renderpass and its subpass dependencies
   void setupRenderpass()
   {
      VkAttachmentDescription attachmentDescriptions[2];

      // Color attachment
      attachmentDescriptions[0] = VkAttachmentDescription();
      attachmentDescriptions[0].format = swapchainFormat.format;
      attachmentDescriptions[0].samples = VK_SAMPLE_COUNT_1_BIT;
      attachmentDescriptions[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
      attachmentDescriptions[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
      attachmentDescriptions[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
      attachmentDescriptions[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
      attachmentDescriptions[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
      attachmentDescriptions[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

      // Depth attachment
      attachmentDescriptions[1] = VkAttachmentDescription();
      attachmentDescriptions[1].format = depthFormat;
      attachmentDescriptions[1].samples = VK_SAMPLE_COUNT_1_BIT;
      attachmentDescriptions[1].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
      attachmentDescriptions[1].storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
      attachmentDescriptions[1].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
      attachmentDescriptions[1].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
      attachmentDescriptions[1].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
      attachmentDescriptions[1].finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

      VkAttachmentReference attachmentReferences[2];

      attachmentReferences[0] = VkAttachmentReference();
      attachmentReferences[0].attachment = 0;
      attachmentReferences[0].layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

      attachmentReferences[1] = VkAttachmentReference();
      attachmentReferences[1].attachment = 1;
      attachmentReferences[1].layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

      // Set up the renderpass to depend on commands that execute before the renderpass begins
      VkSubpassDescription subpassDescription = VkSubpassDescription();
      subpassDescription.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
      subpassDescription.colorAttachmentCount = 1;
      subpassDescription.pColorAttachments = &attachmentReferences[0];
      subpassDescription.pDepthStencilAttachment = &attachmentReferences[1];

      VkSubpassDependency subpassDependency = VkSubpassDependency();
      subpassDependency.srcSubpass = VK_SUBPASS_EXTERNAL;
      subpassDependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
      subpassDependency.srcAccessMask = 0;
      subpassDependency.dstSubpass = 0;
      subpassDependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
      subpassDependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

      VkRenderPassCreateInfo renderPassCreateInfo = VkRenderPassCreateInfo();
      renderPassCreateInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
      renderPassCreateInfo.attachmentCount = 2;
      renderPassCreateInfo.pAttachments = attachmentDescriptions;
      renderPassCreateInfo.subpassCount = 1;
      renderPassCreateInfo.pSubpasses = &subpassDescription;
      renderPassCreateInfo.dependencyCount = 1;
      renderPassCreateInfo.pDependencies = &subpassDependency;

      // Create the renderpass
      if (vkCreateRenderPass(device, &renderPassCreateInfo, 0, &renderPass) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }
   }

   // Set up uniform buffer and texture sampler descriptor set layouts
   void setupDescriptorSetLayout()
   {
      VkDescriptorSetLayoutBinding descriptorSetLayoutBindings[2];

      // Layout binding for uniform buffer
      descriptorSetLayoutBindings[0] = VkDescriptorSetLayoutBinding();
      descriptorSetLayoutBindings[0].binding = 0;
      descriptorSetLayoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
      descriptorSetLayoutBindings[0].descriptorCount = 1;
      descriptorSetLayoutBindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

      // Layout binding for texture sampler
      descriptorSetLayoutBindings[1] = VkDescriptorSetLayoutBinding();
      descriptorSetLayoutBindings[1].binding = 1;
      descriptorSetLayoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
      descriptorSetLayoutBindings[1].descriptorCount = 1;
      descriptorSetLayoutBindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;

      VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo = VkDescriptorSetLayoutCreateInfo();
      descriptorSetLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
      descriptorSetLayoutCreateInfo.bindingCount = 2;
      descriptorSetLayoutCreateInfo.pBindings = descriptorSetLayoutBindings;

      // Create descriptor set layout
      if (vkCreateDescriptorSetLayout(device, &descriptorSetLayoutCreateInfo, 0, &descriptorSetLayout) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }
   }

   // Set up pipeline layout
   void setupPipelineLayout() {
      VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = VkPipelineLayoutCreateInfo();
      pipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
      pipelineLayoutCreateInfo.setLayoutCount = 1;
      pipelineLayoutCreateInfo.pSetLayouts = &descriptorSetLayout;

      // Create pipeline layout
      if (vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, 0, &pipelineLayout) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }
   }

   // Set up rendering pipeline
   void setupPipeline() {
      // Set up how the vertex shader pulls data out of our vertex buffer
      VkVertexInputBindingDescription vertexInputBindingDescription = VkVertexInputBindingDescription();
      vertexInputBindingDescription.binding = 0;
      vertexInputBindingDescription.stride = sizeof(float) * 9;
      vertexInputBindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

      // Set up how the vertex buffer data is interpreted as attributes by the vertex shader
      VkVertexInputAttributeDescription vertexInputAttributeDescriptions[3];

      // Position attribute
      vertexInputAttributeDescriptions[0] = VkVertexInputAttributeDescription();
      vertexInputAttributeDescriptions[0].binding = 0;
      vertexInputAttributeDescriptions[0].location = 0;
      vertexInputAttributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
      vertexInputAttributeDescriptions[0].offset = sizeof(float) * 0;

      // Color attribute
      vertexInputAttributeDescriptions[1] = VkVertexInputAttributeDescription();
      vertexInputAttributeDescriptions[1].binding = 0;
      vertexInputAttributeDescriptions[1].location = 1;
      vertexInputAttributeDescriptions[1].format = VK_FORMAT_R32G32B32A32_SFLOAT;
      vertexInputAttributeDescriptions[1].offset = sizeof(float) * 3;

      // Texture coordinate attribute
      vertexInputAttributeDescriptions[2] = VkVertexInputAttributeDescription();
      vertexInputAttributeDescriptions[2].binding = 0;
      vertexInputAttributeDescriptions[2].location = 2;
      vertexInputAttributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT;
      vertexInputAttributeDescriptions[2].offset = sizeof(float) * 7;

      VkPipelineVertexInputStateCreateInfo vertexInputStateCreateInfo = VkPipelineVertexInputStateCreateInfo();
      vertexInputStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
      vertexInputStateCreateInfo.vertexBindingDescriptionCount = 1;
      vertexInputStateCreateInfo.pVertexBindingDescriptions = &vertexInputBindingDescription;
      vertexInputStateCreateInfo.vertexAttributeDescriptionCount = 3;
      vertexInputStateCreateInfo.pVertexAttributeDescriptions = vertexInputAttributeDescriptions;

      // We want to generate a triangle list with our vertex data
      VkPipelineInputAssemblyStateCreateInfo inputAssemblyStateCreateInfo = VkPipelineInputAssemblyStateCreateInfo();
      inputAssemblyStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
      inputAssemblyStateCreateInfo.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
      inputAssemblyStateCreateInfo.primitiveRestartEnable = VK_FALSE;

      // Set up the viewport
      VkViewport viewport = VkViewport();
      viewport.x = 0.0f;
      viewport.y = 0.0f;
      viewport.width = static_cast<float>(swapchainExtent.width);
      viewport.height = static_cast<float>(swapchainExtent.height);
      viewport.minDepth = 0.0f;
      viewport.maxDepth = 1.f;

      // Set up the scissor region
      VkRect2D scissor = VkRect2D();
      scissor.offset.x = 0;
      scissor.offset.y = 0;
      scissor.extent = swapchainExtent;

      VkPipelineViewportStateCreateInfo pipelineViewportStateCreateInfo = VkPipelineViewportStateCreateInfo();
      pipelineViewportStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
      pipelineViewportStateCreateInfo.viewportCount = 1;
      pipelineViewportStateCreateInfo.pViewports = &viewport;
      pipelineViewportStateCreateInfo.scissorCount = 1;
      pipelineViewportStateCreateInfo.pScissors = &scissor;

      // Set up rasterization parameters: fill polygons, no backface culling, front face is counter-clockwise
      VkPipelineRasterizationStateCreateInfo pipelineRasterizationStateCreateInfo = VkPipelineRasterizationStateCreateInfo();
      pipelineRasterizationStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
      pipelineRasterizationStateCreateInfo.depthClampEnable = VK_FALSE;
      pipelineRasterizationStateCreateInfo.rasterizerDiscardEnable = VK_FALSE;
      pipelineRasterizationStateCreateInfo.polygonMode = VK_POLYGON_MODE_FILL;
      pipelineRasterizationStateCreateInfo.lineWidth = 1.0f;
      pipelineRasterizationStateCreateInfo.cullMode = VK_CULL_MODE_NONE;
      pipelineRasterizationStateCreateInfo.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
      pipelineRasterizationStateCreateInfo.depthBiasEnable = VK_FALSE;

      // Enable depth testing and disable scissor testing
      VkPipelineDepthStencilStateCreateInfo pipelineDepthStencilStateCreateInfo = VkPipelineDepthStencilStateCreateInfo();
      pipelineDepthStencilStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
      pipelineDepthStencilStateCreateInfo.depthTestEnable = VK_TRUE;
      pipelineDepthStencilStateCreateInfo.depthWriteEnable = VK_TRUE;
      pipelineDepthStencilStateCreateInfo.depthCompareOp = VK_COMPARE_OP_LESS;
      pipelineDepthStencilStateCreateInfo.depthBoundsTestEnable = VK_FALSE;
      pipelineDepthStencilStateCreateInfo.stencilTestEnable = VK_FALSE;

      // Enable multi-sampling
      VkPipelineMultisampleStateCreateInfo pipelineMultisampleStateCreateInfo = VkPipelineMultisampleStateCreateInfo();
      pipelineMultisampleStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
      pipelineMultisampleStateCreateInfo.sampleShadingEnable = VK_FALSE;
      pipelineMultisampleStateCreateInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;

      // Set up blending parameters
      VkPipelineColorBlendAttachmentState pipelineColorBlendAttachmentState = VkPipelineColorBlendAttachmentState();
      pipelineColorBlendAttachmentState.blendEnable = VK_TRUE;
      pipelineColorBlendAttachmentState.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
      pipelineColorBlendAttachmentState.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
      pipelineColorBlendAttachmentState.colorBlendOp = VK_BLEND_OP_ADD;
      pipelineColorBlendAttachmentState.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
      pipelineColorBlendAttachmentState.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
      pipelineColorBlendAttachmentState.alphaBlendOp = VK_BLEND_OP_ADD;
      pipelineColorBlendAttachmentState.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;

      VkPipelineColorBlendStateCreateInfo pipelineColorBlendStateCreateInfo = VkPipelineColorBlendStateCreateInfo();
      pipelineColorBlendStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
      pipelineColorBlendStateCreateInfo.logicOpEnable = VK_FALSE;
      pipelineColorBlendStateCreateInfo.attachmentCount = 1;
      pipelineColorBlendStateCreateInfo.pAttachments = &pipelineColorBlendAttachmentState;

      VkGraphicsPipelineCreateInfo graphicsPipelineCreateInfo = VkGraphicsPipelineCreateInfo();
      graphicsPipelineCreateInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
      graphicsPipelineCreateInfo.stageCount = 2;
      graphicsPipelineCreateInfo.pStages = shaderStages;
      graphicsPipelineCreateInfo.pVertexInputState = &vertexInputStateCreateInfo;
      graphicsPipelineCreateInfo.pInputAssemblyState = &inputAssemblyStateCreateInfo;
      graphicsPipelineCreateInfo.pViewportState = &pipelineViewportStateCreateInfo;
      graphicsPipelineCreateInfo.pRasterizationState = &pipelineRasterizationStateCreateInfo;
      graphicsPipelineCreateInfo.pDepthStencilState = &pipelineDepthStencilStateCreateInfo;
      graphicsPipelineCreateInfo.pMultisampleState = &pipelineMultisampleStateCreateInfo;
      graphicsPipelineCreateInfo.pColorBlendState = &pipelineColorBlendStateCreateInfo;
      graphicsPipelineCreateInfo.layout = pipelineLayout;
      graphicsPipelineCreateInfo.renderPass = renderPass;
      graphicsPipelineCreateInfo.subpass = 0;

      // Create our graphics pipeline
      if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &graphicsPipelineCreateInfo, 0, &graphicsPipeline) != VK_SUCCESS) {
         vulkanAvailable = false;
      }
   }

   // Use our renderpass and swapchain images to create the corresponding framebuffers
   void setupFramebuffers() {
      swapchainFramebuffers.resize(swapchainImageViews.size());

      VkFramebufferCreateInfo framebufferCreateInfo = VkFramebufferCreateInfo();
      framebufferCreateInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
      framebufferCreateInfo.renderPass = renderPass;
      framebufferCreateInfo.attachmentCount = 2;
      framebufferCreateInfo.width = swapchainExtent.width;
      framebufferCreateInfo.height = swapchainExtent.height;
      framebufferCreateInfo.layers = 1;

      for (std::size_t i = 0; i < swapchainFramebuffers.size(); i++) {
         // Each framebuffer consists of a corresponding swapchain image and the shared depth image
         VkImageView attachments[] = { swapchainImageViews[i], depthImageView };

         framebufferCreateInfo.pAttachments = attachments;

         // Create the framebuffer
         if (vkCreateFramebuffer(device, &framebufferCreateInfo, 0, &swapchainFramebuffers[i]) != VK_SUCCESS) {
            vulkanAvailable = false;
            return;
         }
      }
   }

   // Set up our command pool
   void setupCommandPool() {
      // We want to be able to reset command buffers after submitting them
      VkCommandPoolCreateInfo commandPoolCreateInfo = VkCommandPoolCreateInfo();
      commandPoolCreateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
      commandPoolCreateInfo.queueFamilyIndex = queueFamilyIndex;
      commandPoolCreateInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;

      // Create our command pool
      if (vkCreateCommandPool(device, &commandPoolCreateInfo, 0, &commandPool) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }
   }

   // Helper to create a generic buffer with the specified size, usage and memory flags
   bool createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& memory) {
      // We only have a single queue so we can request exclusive access
      VkBufferCreateInfo bufferCreateInfo = VkBufferCreateInfo();
      bufferCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
      bufferCreateInfo.size = size;
      bufferCreateInfo.usage = usage;
      bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

      // Create the buffer, this does not allocate any memory for it yet
      if (vkCreateBuffer(device, &bufferCreateInfo, 0, &buffer) != VK_SUCCESS) {
         return false;
      }

      // Check what kind of memory we need to request from the GPU
      VkMemoryRequirements memoryRequirements = VkMemoryRequirements();
      vkGetBufferMemoryRequirements(device, buffer, &memoryRequirements);

      // Check what GPU memory type is available for us to allocate out of
      VkPhysicalDeviceMemoryProperties memoryProperties = VkPhysicalDeviceMemoryProperties();
      vkGetPhysicalDeviceMemoryProperties(gpu, &memoryProperties);

      uint32_t memoryType = 0;

      for (; memoryType < memoryProperties.memoryTypeCount; memoryType++) {
         if ((memoryRequirements.memoryTypeBits & (1 << memoryType)) &&
               ((memoryProperties.memoryTypes[memoryType].propertyFlags & properties) == properties)) {
            break;
         }
      }

      if (memoryType == memoryProperties.memoryTypeCount) {
         return false;
      }

      VkMemoryAllocateInfo memoryAllocateInfo = VkMemoryAllocateInfo();
      memoryAllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
      memoryAllocateInfo.allocationSize = memoryRequirements.size;
      memoryAllocateInfo.memoryTypeIndex = memoryType;

      // Allocate the memory out of the GPU pool for the required memory type
      if (vkAllocateMemory(device, &memoryAllocateInfo, 0, &memory) != VK_SUCCESS) {
         return false;
      }

      // Bind the allocated memory to our buffer object
      if (vkBindBufferMemory(device, buffer, memory, 0) != VK_SUCCESS) {
         return false;
      }

      return true;
   }

   // Helper to copy the contents of one buffer to another buffer
   bool copyBuffer(VkBuffer dst, VkBuffer src, VkDeviceSize size) {
      // Allocate a primary command buffer out of our command pool
      VkCommandBufferAllocateInfo commandBufferAllocateInfo = VkCommandBufferAllocateInfo();
      commandBufferAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
      commandBufferAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
      commandBufferAllocateInfo.commandPool = commandPool;
      commandBufferAllocateInfo.commandBufferCount = 1;

      VkCommandBuffer commandBuffer;

      if (vkAllocateCommandBuffers(device, &commandBufferAllocateInfo, &commandBuffer) != VK_SUCCESS) {
         return false;
      }

      // Begin the command buffer
      VkCommandBufferBeginInfo commandBufferBeginInfo = VkCommandBufferBeginInfo();
      commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
      commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

      if (vkBeginCommandBuffer(commandBuffer, &commandBufferBeginInfo) != VK_SUCCESS) {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         return false;
      }

      // Add our buffer copy command
      VkBufferCopy bufferCopy = VkBufferCopy();
      bufferCopy.srcOffset = 0;
      bufferCopy.dstOffset = 0;
      bufferCopy.size = size;

      vkCmdCopyBuffer(commandBuffer, src, dst, 1, &bufferCopy);

      // End and submit the command buffer
      vkEndCommandBuffer(commandBuffer);

      VkSubmitInfo submitInfo = VkSubmitInfo();
      submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
      submitInfo.commandBufferCount = 1;
      submitInfo.pCommandBuffers = &commandBuffer;

      if (vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         return false;
      }

      // Ensure the command buffer has been processed
      if (vkQueueWaitIdle(queue) != VK_SUCCESS) {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         return false;
      }

      // Free the command buffer
      vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

      return true;
   }

   // Create our vertex buffer and upload its data
   void setupVertexBuffer() {
      float vertexData[] = {
         // X      Y      Z     R     G     B     A     U     V
         -0.5f, -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
          0.5f, -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
          0.5f,  0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
         -0.5f,  0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,

         -0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
          0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f,
          0.5f,  0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f,
         -0.5f,  0.5f, -0.5f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f,

          0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f,
          0.5f,  0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
          0.5f,  0.5f,  0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f,
          0.5f, -0.5f,  0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f,

         -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
         -0.5f,  0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,
         -0.5f,  0.5f,  0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f,
         -0.5f, -0.5f,  0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f,

         -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,
          0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f,
          0.5f, -0.5f,  0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
         -0.5f, -0.5f,  0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,

         -0.5f,  0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
          0.5f,  0.5f, -0.5f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,
          0.5f,  0.5f,  0.5f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,
         -0.5f,  0.5f,  0.5f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f
      };

      // Create a staging buffer that is writable by the CPU
      VkBuffer stagingBuffer = 0;
      VkDeviceMemory stagingBufferMemory = 0;

      if (!createBuffer(
         sizeof(vertexData),
         VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
         stagingBuffer,
         stagingBufferMemory
      ))
      {
         vulkanAvailable = false;
         return;
      }

      void* ptr;

      // Map the buffer into our address space
      if (vkMapMemory(device, stagingBufferMemory, 0, sizeof(vertexData), 0, &ptr) != VK_SUCCESS)
      {
         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Copy the vertex data into the buffer
      std::memcpy(ptr, vertexData, sizeof(vertexData));

      // Unmap the buffer
      vkUnmapMemory(device, stagingBufferMemory);

      // Create the GPU local vertex buffer
      if (!createBuffer(
         sizeof(vertexData),
         VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
         VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
         vertexBuffer,
         vertexBufferMemory
      ))
      {
         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Copy the contents of the staging buffer into the GPU vertex buffer
      vulkanAvailable = copyBuffer(vertexBuffer, stagingBuffer, sizeof(vertexData));

      // Free the staging buffer and its memory
      vkFreeMemory(device, stagingBufferMemory, 0);
      vkDestroyBuffer(device, stagingBuffer, 0);
   }

   // Create our index buffer and upload its data
   void setupIndexBuffer()
   {
      uint16_t indexData[] = {
          0,  1,  2,
          2,  3,  0,

          4,  5,  6,
          6,  7,  4,

          8,  9,  10,
          10, 11, 8,

          12, 13, 14,
          14, 15, 12,

          16, 17, 18,
          18, 19, 16,

          20, 21, 22,
          22, 23, 20
      };

      // Create a staging buffer that is writable by the CPU
      VkBuffer stagingBuffer = 0;
      VkDeviceMemory stagingBufferMemory = 0;

      if (!createBuffer(
         sizeof(indexData),
         VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
         stagingBuffer,
         stagingBufferMemory
      ))
      {
         vulkanAvailable = false;
         return;
      }

      void* ptr;

      // Map the buffer into our address space
      if (vkMapMemory(device, stagingBufferMemory, 0, sizeof(indexData), 0, &ptr) != VK_SUCCESS)
      {
         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Copy the index data into the buffer
      std::memcpy(ptr, indexData, sizeof(indexData));

      // Unmap the buffer
      vkUnmapMemory(device, stagingBufferMemory);

      // Create the GPU local index buffer
      if (!createBuffer(
         sizeof(indexData),
         VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
         VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
         indexBuffer,
         indexBufferMemory
      ))
      {
         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Copy the contents of the staging buffer into the GPU index buffer
      vulkanAvailable = copyBuffer(indexBuffer, stagingBuffer, sizeof(indexData));

      // Free the staging buffer and its memory
      vkFreeMemory(device, stagingBufferMemory, 0);
      vkDestroyBuffer(device, stagingBuffer, 0);
   }

   // Create our uniform buffer but don't upload any data yet
   void setupUniformBuffers()
   {
      // Create a uniform buffer for every frame that might be in flight to prevent clobbering
      for (size_t i = 0; i < swapchainImages.size(); i++)
      {
         uniformBuffers.push_back(0);
         uniformBuffersMemory.push_back(0);

         // The uniform buffer will be host visible and coherent since we use it for streaming data every frame
         if (!createBuffer(
            sizeof(Matrix) * 3,
            VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
            uniformBuffers[i],
            uniformBuffersMemory[i]
         ))
         {
            vulkanAvailable = false;
            return;
         }
      }
   }

   // Helper to create a generic image with the specified size, format, usage and memory flags
   bool createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory)
   {
      // We only have a single queue so we can request exclusive access
      VkImageCreateInfo imageCreateInfo = VkImageCreateInfo();
      imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
      imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
      imageCreateInfo.extent.width = width;
      imageCreateInfo.extent.height = height;
      imageCreateInfo.extent.depth = 1;
      imageCreateInfo.mipLevels = 1;
      imageCreateInfo.arrayLayers = 1;
      imageCreateInfo.format = format;
      imageCreateInfo.tiling = tiling;
      imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
      imageCreateInfo.usage = usage;
      imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT;
      imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

      // Create the image, this does not allocate any memory for it yet
      if (vkCreateImage(device, &imageCreateInfo, 0, &image) != VK_SUCCESS)
         return false;

      // Check what kind of memory we need to request from the GPU
      VkMemoryRequirements memoryRequirements = VkMemoryRequirements();
      vkGetImageMemoryRequirements(device, image, &memoryRequirements);

      // Check what GPU memory type is available for us to allocate out of
      VkPhysicalDeviceMemoryProperties memoryProperties = VkPhysicalDeviceMemoryProperties();
      vkGetPhysicalDeviceMemoryProperties(gpu, &memoryProperties);

      uint32_t memoryType = 0;

      for (; memoryType < memoryProperties.memoryTypeCount; memoryType++)
      {
         if ((memoryRequirements.memoryTypeBits & (1 << memoryType)) &&
            ((memoryProperties.memoryTypes[memoryType].propertyFlags & properties) == properties))
            break;
      }

      if (memoryType == memoryProperties.memoryTypeCount)
         return false;

      VkMemoryAllocateInfo memoryAllocateInfo = VkMemoryAllocateInfo();
      memoryAllocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
      memoryAllocateInfo.allocationSize = memoryRequirements.size;
      memoryAllocateInfo.memoryTypeIndex = memoryType;

      // Allocate the memory out of the GPU pool for the required memory type
      if (vkAllocateMemory(device, &memoryAllocateInfo, 0, &imageMemory) != VK_SUCCESS)
         return false;

      // Bind the allocated memory to our image object
      if (vkBindImageMemory(device, image, imageMemory, 0) != VK_SUCCESS)
         return false;

      return true;
   }

   // Create our depth image and transition it into the proper layout
   void setupDepthImage()
   {
      // Create our depth image
      if (!createImage(
         swapchainExtent.width,
         swapchainExtent.height,
         depthFormat,
         VK_IMAGE_TILING_OPTIMAL,
         VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
         VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
         depthImage,
         depthImageMemory
      ))
      {
         vulkanAvailable = false;
         return;
      }

      // Allocate a command buffer
      VkCommandBufferAllocateInfo commandBufferAllocateInfo = VkCommandBufferAllocateInfo();
      commandBufferAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
      commandBufferAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
      commandBufferAllocateInfo.commandPool = commandPool;
      commandBufferAllocateInfo.commandBufferCount = 1;

      VkCommandBuffer commandBuffer;

      if (vkAllocateCommandBuffers(device, &commandBufferAllocateInfo, &commandBuffer) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }

      // Begin the command buffer
      VkCommandBufferBeginInfo commandBufferBeginInfo = VkCommandBufferBeginInfo();
      commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
      commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

      VkSubmitInfo submitInfo = VkSubmitInfo();
      submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
      submitInfo.commandBufferCount = 1;
      submitInfo.pCommandBuffers = &commandBuffer;

      if (vkBeginCommandBuffer(commandBuffer, &commandBufferBeginInfo) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vulkanAvailable = false;
         return;
      }

      // Submit a barrier to transition the image layout to depth stencil optimal
      VkImageMemoryBarrier barrier = VkImageMemoryBarrier();
      barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
      barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
      barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
      barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
      barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
      barrier.image = depthImage;
      barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | ((depthFormat == VK_FORMAT_D32_SFLOAT) ? 0 : VK_IMAGE_ASPECT_STENCIL_BIT);
      barrier.subresourceRange.baseMipLevel = 0;
      barrier.subresourceRange.levelCount = 1;
      barrier.subresourceRange.baseArrayLayer = 0;
      barrier.subresourceRange.layerCount = 1;
      barrier.srcAccessMask = 0;
      barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;

      vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, 0, 0, 0, 0, 0, 1, &barrier);

      // End and submit the command buffer
      if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vulkanAvailable = false;
         return;
      }

      if (vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vulkanAvailable = false;
         return;
      }

      // Ensure the command buffer has been processed
      if (vkQueueWaitIdle(queue) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vulkanAvailable = false;
         return;
      }

      // Free the command buffer
      vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
   }

   // Create an image view for our depth image
   void setupDepthImageView()
   {
      VkImageViewCreateInfo imageViewCreateInfo = VkImageViewCreateInfo();
      imageViewCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
      imageViewCreateInfo.image = depthImage;
      imageViewCreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
      imageViewCreateInfo.format = depthFormat;
      imageViewCreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | ((depthFormat == VK_FORMAT_D32_SFLOAT) ? 0 : VK_IMAGE_ASPECT_STENCIL_BIT);
      imageViewCreateInfo.subresourceRange.baseMipLevel = 0;
      imageViewCreateInfo.subresourceRange.levelCount = 1;
      imageViewCreateInfo.subresourceRange.baseArrayLayer = 0;
      imageViewCreateInfo.subresourceRange.layerCount = 1;

      // Create the depth image view
      if (vkCreateImageView(device, &imageViewCreateInfo, 0, &depthImageView) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }
   }

   // Create an image for our texture data
   void setupTextureImage()
   {
      // Load the image data
      sf::Image imageData;

      if (!imageData.loadFromFile("resources/logo.png"))
      {
         vulkanAvailable = false;
         return;
      }

      // Create a staging buffer to transfer the data with
      VkDeviceSize imageSize = imageData.getSize().x * imageData.getSize().y * 4;

      VkBuffer stagingBuffer;
      VkDeviceMemory stagingBufferMemory;
      createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

      void* ptr;

      // Map the buffer into our address space
      if (vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &ptr) != VK_SUCCESS)
      {
         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Copy the image data into the buffer
      std::memcpy(ptr, imageData.getPixelsPtr(), static_cast<size_t>(imageSize));

      // Unmap the buffer
      vkUnmapMemory(device, stagingBufferMemory);

      // Create a GPU local image
      if (!createImage(
         imageData.getSize().x,
         imageData.getSize().y,
         VK_FORMAT_R8G8B8A8_UNORM,
         VK_IMAGE_TILING_OPTIMAL,
         VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
         VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
         textureImage,
         textureImageMemory
      ))
      {
         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Create a command buffer
      VkCommandBufferAllocateInfo commandBufferAllocateInfo = VkCommandBufferAllocateInfo();
      commandBufferAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
      commandBufferAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
      commandBufferAllocateInfo.commandPool = commandPool;
      commandBufferAllocateInfo.commandBufferCount = 1;

      VkCommandBuffer commandBuffer;

      if (vkAllocateCommandBuffers(device, &commandBufferAllocateInfo, &commandBuffer) != VK_SUCCESS)
      {
         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Begin the command buffer
      VkCommandBufferBeginInfo commandBufferBeginInfo = VkCommandBufferBeginInfo();
      commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
      commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

      VkSubmitInfo submitInfo = VkSubmitInfo();
      submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
      submitInfo.commandBufferCount = 1;
      submitInfo.pCommandBuffers = &commandBuffer;

      if (vkBeginCommandBuffer(commandBuffer, &commandBufferBeginInfo) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Submit a barrier to transition the image layout to transfer destionation optimal
      VkImageMemoryBarrier barrier = VkImageMemoryBarrier();
      barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
      barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
      barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
      barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
      barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
      barrier.image = textureImage;
      barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
      barrier.subresourceRange.baseMipLevel = 0;
      barrier.subresourceRange.levelCount = 1;
      barrier.subresourceRange.baseArrayLayer = 0;
      barrier.subresourceRange.layerCount = 1;
      barrier.srcAccessMask = 0;
      barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

      vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, 0, 0, 0, 1, &barrier);

      if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      if (vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Ensure the command buffer has been processed
      if (vkQueueWaitIdle(queue) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Begin the command buffer
      if (vkBeginCommandBuffer(commandBuffer, &commandBufferBeginInfo) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Copy the staging buffer contents into the image
      VkBufferImageCopy bufferImageCopy = VkBufferImageCopy();
      bufferImageCopy.bufferOffset = 0;
      bufferImageCopy.bufferRowLength = 0;
      bufferImageCopy.bufferImageHeight = 0;
      bufferImageCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
      bufferImageCopy.imageSubresource.mipLevel = 0;
      bufferImageCopy.imageSubresource.baseArrayLayer = 0;
      bufferImageCopy.imageSubresource.layerCount = 1;
      bufferImageCopy.imageOffset.x = 0;
      bufferImageCopy.imageOffset.y = 0;
      bufferImageCopy.imageOffset.z = 0;
      bufferImageCopy.imageExtent.width = imageData.getSize().x;
      bufferImageCopy.imageExtent.height = imageData.getSize().y;
      bufferImageCopy.imageExtent.depth = 1;

      vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bufferImageCopy);

      // End and submit the command buffer
      if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      if (vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Ensure the command buffer has been processed
      if (vkQueueWaitIdle(queue) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Begin the command buffer
      if (vkBeginCommandBuffer(commandBuffer, &commandBufferBeginInfo) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Submit a barrier to transition the image layout from transfer destionation optimal to shader read-only optimal
      barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
      barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
      barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
      barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

      vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, 0, 0, 0, 1, &barrier);

      // End and submit the command buffer
      if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      if (vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Ensure the command buffer has been processed
      if (vkQueueWaitIdle(queue) != VK_SUCCESS)
      {
         vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

         vkFreeMemory(device, stagingBufferMemory, 0);
         vkDestroyBuffer(device, stagingBuffer, 0);

         vulkanAvailable = false;
         return;
      }

      // Free the command buffer
      vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);

      vkFreeMemory(device, stagingBufferMemory, 0);
      vkDestroyBuffer(device, stagingBuffer, 0);
   }

   // Create an image view for our texture
   void setupTextureImageView()
   {
      VkImageViewCreateInfo imageViewCreateInfo = VkImageViewCreateInfo();
      imageViewCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
      imageViewCreateInfo.image = textureImage;
      imageViewCreateInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
      imageViewCreateInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
      imageViewCreateInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
      imageViewCreateInfo.subresourceRange.baseMipLevel = 0;
      imageViewCreateInfo.subresourceRange.levelCount = 1;
      imageViewCreateInfo.subresourceRange.baseArrayLayer = 0;
      imageViewCreateInfo.subresourceRange.layerCount = 1;

      // Create our texture image view
      if (vkCreateImageView(device, &imageViewCreateInfo, 0, &textureImageView) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }
   }

   // Create a sampler for our texture
   void setupTextureSampler()
   {
      // Sampler parameters: linear min/mag filtering, 4x anisotropic
      VkSamplerCreateInfo samplerCreateInfo = VkSamplerCreateInfo();
      samplerCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
      samplerCreateInfo.magFilter = VK_FILTER_LINEAR;
      samplerCreateInfo.minFilter = VK_FILTER_LINEAR;
      samplerCreateInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
      samplerCreateInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
      samplerCreateInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
      samplerCreateInfo.anisotropyEnable = VK_TRUE;
      samplerCreateInfo.maxAnisotropy = 4;
      samplerCreateInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
      samplerCreateInfo.unnormalizedCoordinates = VK_FALSE;
      samplerCreateInfo.compareEnable = VK_FALSE;
      samplerCreateInfo.compareOp = VK_COMPARE_OP_ALWAYS;
      samplerCreateInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
      samplerCreateInfo.mipLodBias = 0.0f;
      samplerCreateInfo.minLod = 0.0f;
      samplerCreateInfo.maxLod = 0.0f;

      // Create our sampler
      if (vkCreateSampler(device, &samplerCreateInfo, 0, &textureSampler) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }
   }

   // Set up our descriptor pool
   void setupDescriptorPool()
   {
      // We need to allocate as many descriptor sets as we have frames in flight
      VkDescriptorPoolSize descriptorPoolSizes[2];

      descriptorPoolSizes[0] = VkDescriptorPoolSize();
      descriptorPoolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
      descriptorPoolSizes[0].descriptorCount = static_cast<uint32_t>(swapchainImages.size());

      descriptorPoolSizes[1] = VkDescriptorPoolSize();
      descriptorPoolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
      descriptorPoolSizes[1].descriptorCount = static_cast<uint32_t>(swapchainImages.size());

      VkDescriptorPoolCreateInfo descriptorPoolCreateInfo = VkDescriptorPoolCreateInfo();
      descriptorPoolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
      descriptorPoolCreateInfo.poolSizeCount = 2;
      descriptorPoolCreateInfo.pPoolSizes = descriptorPoolSizes;
      descriptorPoolCreateInfo.maxSets = static_cast<uint32_t>(swapchainImages.size());

      // Create the descriptor pool
      if (vkCreateDescriptorPool(device, &descriptorPoolCreateInfo, 0, &descriptorPool) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }
   }

   // Set up our descriptor sets
   void setupDescriptorSets()
   {
      // Allocate a descriptor set for each frame in flight
      std::vector<VkDescriptorSetLayout> descriptorSetLayouts(swapchainImages.size(), descriptorSetLayout);

      VkDescriptorSetAllocateInfo descriptorSetAllocateInfo = VkDescriptorSetAllocateInfo();
      descriptorSetAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
      descriptorSetAllocateInfo.descriptorPool = descriptorPool;
      descriptorSetAllocateInfo.descriptorSetCount = static_cast<uint32_t>(swapchainImages.size());
      descriptorSetAllocateInfo.pSetLayouts = &descriptorSetLayouts[0];

      descriptorSets.resize(swapchainImages.size());

      if (vkAllocateDescriptorSets(device, &descriptorSetAllocateInfo, &descriptorSets[0]) != VK_SUCCESS)
      {
         descriptorSets.clear();

         vulkanAvailable = false;
         return;
      }

      // For every descriptor set, set up the bindings to our uniform buffer and texture sampler
      for (std::size_t i = 0; i < descriptorSets.size(); i++)
      {
         VkWriteDescriptorSet writeDescriptorSets[2];

         // Uniform buffer binding information
         VkDescriptorBufferInfo descriptorBufferInfo = VkDescriptorBufferInfo();
         descriptorBufferInfo.buffer = uniformBuffers[i];
         descriptorBufferInfo.offset = 0;
         descriptorBufferInfo.range = sizeof(Matrix) * 3;

         writeDescriptorSets[0] = VkWriteDescriptorSet();
         writeDescriptorSets[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
         writeDescriptorSets[0].dstSet = descriptorSets[i];
         writeDescriptorSets[0].dstBinding = 0;
         writeDescriptorSets[0].dstArrayElement = 0;
         writeDescriptorSets[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
         writeDescriptorSets[0].descriptorCount = 1;
         writeDescriptorSets[0].pBufferInfo = &descriptorBufferInfo;

         // Texture sampler binding information
         VkDescriptorImageInfo descriptorImageInfo = VkDescriptorImageInfo();
         descriptorImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
         descriptorImageInfo.imageView = textureImageView;
         descriptorImageInfo.sampler = textureSampler;

         writeDescriptorSets[1] = VkWriteDescriptorSet();
         writeDescriptorSets[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
         writeDescriptorSets[1].dstSet = descriptorSets[i];
         writeDescriptorSets[1].dstBinding = 1;
         writeDescriptorSets[1].dstArrayElement = 0;
         writeDescriptorSets[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
         writeDescriptorSets[1].descriptorCount = 1;
         writeDescriptorSets[1].pImageInfo = &descriptorImageInfo;

         // Update the desciptor set
         vkUpdateDescriptorSets(device, 2, writeDescriptorSets, 0, 0);
      }
   }

   // Set up the command buffers we use for drawing each frame
   void setupCommandBuffers()
   {
      // We need a command buffer for every frame in flight
      commandBuffers.resize(swapchainFramebuffers.size());

      // These are primary command buffers
      VkCommandBufferAllocateInfo commandBufferAllocateInfo = VkCommandBufferAllocateInfo();
      commandBufferAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
      commandBufferAllocateInfo.commandPool = commandPool;
      commandBufferAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
      commandBufferAllocateInfo.commandBufferCount = static_cast<uint32_t>(commandBuffers.size());

      // Allocate the command buffers from our command pool
      if (vkAllocateCommandBuffers(device, &commandBufferAllocateInfo, &commandBuffers[0]) != VK_SUCCESS)
      {
         commandBuffers.clear();
         vulkanAvailable = false;
         return;
      }
   }

   // Set up the commands we need to issue to draw a frame
   void setupDraw()
   {
      // Set up our clear colors
      VkClearValue clearColors[2];

      // Clear color buffer to opaque black
      clearColors[0] = VkClearValue();
      clearColors[0].color.float32[0] = 0.0f;
      clearColors[0].color.float32[1] = 0.0f;
      clearColors[0].color.float32[2] = 0.0f;
      clearColors[0].color.float32[3] = 0.0f;

      // Clear depth to 1.0f
      clearColors[1] = VkClearValue();
      clearColors[1].depthStencil.depth = 1.0f;
      clearColors[1].depthStencil.stencil = 0;

      VkRenderPassBeginInfo renderPassBeginInfo = VkRenderPassBeginInfo();
      renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
      renderPassBeginInfo.renderPass = renderPass;
      renderPassBeginInfo.renderArea.offset.x = 0;
      renderPassBeginInfo.renderArea.offset.y = 0;
      renderPassBeginInfo.renderArea.extent = swapchainExtent;
      renderPassBeginInfo.clearValueCount = 2;
      renderPassBeginInfo.pClearValues = clearColors;

      // Simultaneous use: this command buffer can be resubmitted to a queue before a previous submission is completed
      VkCommandBufferBeginInfo commandBufferBeginInfo = VkCommandBufferBeginInfo();
      commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
      commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;

      // Set up the command buffers for each frame in flight
      for (std::size_t i = 0; i < commandBuffers.size(); i++)
      {
         // Begin the command buffer
         if (vkBeginCommandBuffer(commandBuffers[i], &commandBufferBeginInfo) != VK_SUCCESS)
         {
            vulkanAvailable = false;
            return;
         }

         // Begin the renderpass
         renderPassBeginInfo.framebuffer = swapchainFramebuffers[i];

         vkCmdBeginRenderPass(commandBuffers[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);

         // Bind our graphics pipeline
         vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

         // Bind our vertex buffer
         VkDeviceSize offset = 0;

         vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, &vertexBuffer, &offset);

         // Bind our index buffer
         vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT16);

         // Bind our descriptor sets
         vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[i], 0, 0);

         // Draw our primitives
         vkCmdDrawIndexed(commandBuffers[i], 36, 1, 0, 0, 0);

         // End the renderpass
         vkCmdEndRenderPass(commandBuffers[i]);

         // End the command buffer
         if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS)
         {
            vulkanAvailable = false;
            return;
         }
      }
   }

   // Set up the semaphores we use to synchronize frames among each other
   void setupSemaphores()
   {
      VkSemaphoreCreateInfo semaphoreCreateInfo = VkSemaphoreCreateInfo();
      semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

      // Create a semaphore to track when an swapchain image is available for each frame in flight
      for (int i = 0; i < maxFramesInFlight; i++)
      {
         imageAvailableSemaphores.push_back(0);

         if (vkCreateSemaphore(device, &semaphoreCreateInfo, 0, &imageAvailableSemaphores[i]) != VK_SUCCESS)
         {
            imageAvailableSemaphores.pop_back();
            vulkanAvailable = false;
            return;
         }
      }

      // Create a semaphore to track when rendering is complete for each frame in flight
      for (int i = 0; i < maxFramesInFlight; i++)
      {
         renderFinishedSemaphores.push_back(0);

         if (vkCreateSemaphore(device, &semaphoreCreateInfo, 0, &renderFinishedSemaphores[i]) != VK_SUCCESS)
         {
            renderFinishedSemaphores.pop_back();
            vulkanAvailable = false;
            return;
         }
      }
   }

   // Set up the fences we use to synchronize frames among each other
   void setupFences()
   {
      // Create the fences in the signaled state
      VkFenceCreateInfo fenceCreateInfo = VkFenceCreateInfo();
      fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
      fenceCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;

      // Create a fence to track when queue submission is complete for each frame in flight
      for (int i = 0; i < maxFramesInFlight; i++)
      {
         fences.push_back(0);

         if (vkCreateFence(device, &fenceCreateInfo, 0, &fences[i]) != VK_SUCCESS)
         {
            fences.pop_back();
            vulkanAvailable = false;
            return;
         }
      }
   }

   // Update the matrices in our uniform buffer every frame
   void updateUniformBuffer(float elapsed)
   {
      const float pi = 3.14159265359f;

      // Construct the model matrix
      Matrix model = {
          1.0f, 0.0f, 0.0f, 0.0f,
          0.0f, 1.0f, 0.0f, 0.0f,
          0.0f, 0.0f, 1.0f, 0.0f,
          0.0f, 0.0f, 0.0f, 1.0f
      };

      matrixRotateX(model, elapsed * 59.0f * pi / 180.f);
      matrixRotateY(model, elapsed * 83.0f * pi / 180.f);
      matrixRotateZ(model, elapsed * 109.0f * pi / 180.f);

      // Translate the model based on the mouse position
      float x = clamp(sf::Mouse::getPosition(window).x * 2.f / window.getSize().x - 1.f, -1.0f, 1.0f) * 2.0f;
      float y = clamp(-sf::Mouse::getPosition(window).y * 2.f / window.getSize().y + 1.f, -1.0f, 1.0f) * 1.5f;

      model[3][0] -= x;
      model[3][2] += y;

      // Construct the view matrix
      const Vec3 eye = { 0.0f, 4.0f, 0.0f };
      const Vec3 center = { 0.0f, 0.0f, 0.0f };
      const Vec3 up = { 0.0f, 0.0f, 1.0f };

      Matrix view;

      matrixLookAt(view, eye, center, up);

      // Construct the projection matrix
      const float fov = 45.0f;
      const float aspect = static_cast<float>(swapchainExtent.width) / static_cast<float>(swapchainExtent.height);
      const float nearPlane = 0.1f;
      const float farPlane = 10.0f;

      Matrix projection;

      matrixPerspective(projection, fov * pi / 180.f, aspect, nearPlane, farPlane);

      char* ptr;

      // Map the current frame's uniform buffer into our address space
      if (vkMapMemory(device, uniformBuffersMemory[currentFrame], 0, sizeof(Matrix) * 3, 0, reinterpret_cast<void**>(&ptr)) != VK_SUCCESS)
      {
         vulkanAvailable = false;
         return;
      }

      // Copy the matrix data into the current frame's uniform buffer
      std::memcpy(ptr + sizeof(Matrix) * 0, model, sizeof(Matrix));
      std::memcpy(ptr + sizeof(Matrix) * 1, view, sizeof(Matrix));
      std::memcpy(ptr + sizeof(Matrix) * 2, projection, sizeof(Matrix));

      // Unmap the buffer
      vkUnmapMemory(device, uniformBuffersMemory[currentFrame]);
   }

   void draw() {
      uint32_t imageIndex = 0;

      // If the objects we need to submit this frame are still pending, wait here
      vkWaitForFences(device, 1, &fences[currentFrame], VK_TRUE, std::numeric_limits<uint64_t>::max());

      // Get the next image in the swapchain
      VkResult result = vkAcquireNextImageKHR(device, swapchain, std::numeric_limits<uint64_t>::max(), imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

      // Check if we need to re-create the swapchain (e.g. if the window was resized)
      if (result == VK_ERROR_OUT_OF_DATE_KHR) {
         recreateSwapchain();
         swapchainOutOfDate = false;
         return;
      }

      // && result != VK_TIMEOUT && result != VK_NOT_READY
      if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
         throw runtime_error("failed to acquire swap chain image!");
         vulkanAvailable = false;
         return;
      }

      // Wait for the swapchain image to be available in the color attachment stage before submitting the queue
      VkPipelineStageFlags waitStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;

      // Signal the render finished semaphore once the queue has been processed
      VkSubmitInfo submitInfo = VkSubmitInfo();
      submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
      submitInfo.waitSemaphoreCount = 1;
      submitInfo.pWaitSemaphores = &imageAvailableSemaphores[currentFrame];
      submitInfo.pWaitDstStageMask = &waitStages;
      submitInfo.commandBufferCount = 1;
      submitInfo.pCommandBuffers = &commandBuffers[imageIndex];
      submitInfo.signalSemaphoreCount = 1;
      submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentFrame];

      vkResetFences(device, 1, &fences[currentFrame]);

      if (vkQueueSubmit(queue, 1, &submitInfo, fences[currentFrame]) != VK_SUCCESS) {
         vulkanAvailable = false;
         return;
      }

      // Wait for rendering to complete before presenting
      VkPresentInfoKHR presentInfo = VkPresentInfoKHR();
      presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
      presentInfo.waitSemaphoreCount = 1;
      presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentFrame];
      presentInfo.swapchainCount = 1;
      presentInfo.pSwapchains = &swapchain;
      presentInfo.pImageIndices = &imageIndex;

      // Queue presentation
      result = vkQueuePresentKHR(queue, &presentInfo);

      // Check if we need to re-create the swapchain (e.g. if the window was resized)
      if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || swapchainOutOfDate) {
         recreateSwapchain();
         swapchainOutOfDate = false;
      } else if (result != VK_SUCCESS) {
         throw runtime_error("failed to present swap chain image!");
         vulkanAvailable = false;
         return;
      }

      // Make sure to use the next frame's objects next frame
      currentFrame = (currentFrame + 1) % maxFramesInFlight;
   }

   void run()
   {
      sf::Clock clock;

      // Start game loop
      while (window.isOpen())
      {
         // Process events
         sf::Event event;
         while (window.pollEvent(event)) {
            // Close window: exit
            if (event.type == sf::Event::Closed)
               window.close();

            // Escape key: exit
            if ((event.type == sf::Event::KeyPressed) && (event.key.code == sf::Keyboard::Escape))
               window.close();

            // Re-create the swapchain when the window is resized
            if (event.type == sf::Event::Resized)
               swapchainOutOfDate = true;
         }

         if (vulkanAvailable) {
            // Update the uniform buffer (matrices)
            updateUniformBuffer(clock.getElapsedTime().asSeconds());

            // Render the frame
            draw();
         }
      }
   }

private:
   sf::WindowBase window;

   bool vulkanAvailable;

   const int maxFramesInFlight;
   int currentFrame;
   bool swapchainOutOfDate;

   VkInstance instance;
   VkDebugUtilsMessengerEXT debugMessenger;
   VkSurfaceKHR surface;
   VkPhysicalDevice gpu;
   int queueFamilyIndex;
   VkDevice device;
   VkQueue queue;
   VkSurfaceFormatKHR swapchainFormat;
   VkExtent2D swapchainExtent;
   VkSwapchainKHR swapchain;
   std::vector<VkImage> swapchainImages;
   std::vector<VkImageView> swapchainImageViews;
   VkFormat depthFormat;
   VkImage depthImage;
   VkDeviceMemory depthImageMemory;
   VkImageView depthImageView;
   VkShaderModule vertexShaderModule;
   VkShaderModule fragmentShaderModule;
   VkPipelineShaderStageCreateInfo shaderStages[2];
   VkDescriptorSetLayout descriptorSetLayout;
   VkPipelineLayout pipelineLayout;
   VkRenderPass renderPass;
   VkPipeline graphicsPipeline;
   std::vector<VkFramebuffer> swapchainFramebuffers;
   VkCommandPool commandPool;
   VkBuffer vertexBuffer;
   VkDeviceMemory vertexBufferMemory;
   VkBuffer indexBuffer;
   VkDeviceMemory indexBufferMemory;
   std::vector<VkBuffer> uniformBuffers;
   std::vector<VkDeviceMemory> uniformBuffersMemory;
   VkImage textureImage;
   VkDeviceMemory textureImageMemory;
   VkImageView textureImageView;
   VkSampler textureSampler;
   VkDescriptorPool descriptorPool;
   std::vector<VkDescriptorSet> descriptorSets;
   std::vector<VkCommandBuffer> commandBuffers;
   std::vector<VkSemaphore> imageAvailableSemaphores;
   std::vector<VkSemaphore> renderFinishedSemaphores;
   std::vector<VkFence> fences;
};


////////////////////////////////////////////////////////////
/// Entry point of application
///
/// \return Application exit code
///
////////////////////////////////////////////////////////////
int main()
{
   VulkanExample example;

   example.run();

   return EXIT_SUCCESS;
}
