package com.github.hydos.ginger;

import static java.util.stream.Collectors.toSet;
import static org.lwjgl.assimp.Assimp.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.glfw.GLFWVulkan.*;
import static org.lwjgl.stb.STBImage.*;
import static org.lwjgl.system.MemoryStack.*;
import static org.lwjgl.vulkan.KHRSurface.*;
import static org.lwjgl.vulkan.KHRSwapchain.*;
import static org.lwjgl.vulkan.VK10.*;

import java.io.File;
import java.lang.Math;
import java.net.*;
import java.nio.*;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.*;

import org.joml.*;
import org.lwjgl.PointerBuffer;
import org.lwjgl.system.*;
import org.lwjgl.vulkan.*;

import com.github.hydos.ginger.engine.common.info.RenderAPI;
import com.github.hydos.ginger.engine.common.io.Window;
import com.github.hydos.ginger.engine.vulkan.misc.*;
import com.github.hydos.ginger.engine.vulkan.misc.VKModelLoader.VKMesh;
import com.github.hydos.ginger.engine.vulkan.swapchain.VKSwapchainManager;

public class VulkanExample
{
	public static class VulkanDemoGinger2
	{
		private static final int UINT32_MAX = 0xFFFFFFFF;
		private static final long UINT64_MAX = 0xFFFFFFFFFFFFFFFFL;
		private static final int MAX_FRAMES_IN_FLIGHT = 2;
		private static final Set<String> DEVICE_EXTENSIONS = Stream.of(VK_KHR_SWAPCHAIN_EXTENSION_NAME)
			.collect(toSet());

		public static class QueueFamilyIndices
		{
			// We use Integer to use null as the empty value
			public Integer graphicsFamily;
			public Integer presentFamily;

			private boolean isComplete()
			{ return graphicsFamily != null && presentFamily != null; }

			public int[] unique()
			{ return IntStream.of(graphicsFamily, presentFamily).distinct().toArray(); }
		}

		public static class SwapChainSupportDetails
		{
			public VkSurfaceCapabilitiesKHR capabilities;
			public VkSurfaceFormatKHR.Buffer formats;
			public IntBuffer presentModes;
		}

		private static class UniformBufferObject
		{
			private static final int SIZEOF = 3 * 16 * Float.BYTES;
			private Matrix4f model;
			private Matrix4f view;
			private Matrix4f proj;

			public UniformBufferObject()
			{
				model = new Matrix4f();
				view = new Matrix4f();
				proj = new Matrix4f();
			}
		}

		public static class Vertex
		{
			private static final int SIZEOF = (3 + 3 + 2) * Float.BYTES;
			private static final int OFFSETOF_POS = 0;
			private static final int OFFSETOF_COLOR = 3 * Float.BYTES;
			private static final int OFFSETOF_TEXTCOORDS = (3 + 3) * Float.BYTES;
			private Vector3fc pos;
			private Vector3fc color;
			private Vector2fc texCoords;

			public Vertex(Vector3fc pos, Vector3fc color, Vector2fc texCoords)
			{
				this.pos = pos;
				this.color = color;
				this.texCoords = texCoords;
			}

			public static VkVertexInputBindingDescription.Buffer getBindingDescription()
			{
				VkVertexInputBindingDescription.Buffer bindingDescription = VkVertexInputBindingDescription.callocStack(1);
				bindingDescription.binding(0);
				bindingDescription.stride(Vertex.SIZEOF);
				bindingDescription.inputRate(VK_VERTEX_INPUT_RATE_VERTEX);
				return bindingDescription;
			}

			public static VkVertexInputAttributeDescription.Buffer getAttributeDescriptions()
			{
				VkVertexInputAttributeDescription.Buffer attributeDescriptions = VkVertexInputAttributeDescription.callocStack(3);
				// Position
				VkVertexInputAttributeDescription posDescription = attributeDescriptions.get(0);
				posDescription.binding(0);
				posDescription.location(0);
				posDescription.format(VK_FORMAT_R32G32B32_SFLOAT);
				posDescription.offset(OFFSETOF_POS);
				// Color
				VkVertexInputAttributeDescription colorDescription = attributeDescriptions.get(1);
				colorDescription.binding(0);
				colorDescription.location(1);
				colorDescription.format(VK_FORMAT_R32G32B32_SFLOAT);
				colorDescription.offset(OFFSETOF_COLOR);
				// Texture coordinates
				VkVertexInputAttributeDescription texCoordsDescription = attributeDescriptions.get(2);
				texCoordsDescription.binding(0);
				texCoordsDescription.location(2);
				texCoordsDescription.format(VK_FORMAT_R32G32_SFLOAT);
				texCoordsDescription.offset(OFFSETOF_TEXTCOORDS);
				return attributeDescriptions.rewind();
			}
		}
		// ======= FIELDS ======= //

		private VkInstance instance;
		public static long surface;
		public static VkPhysicalDevice physicalDevice;
		public static int msaaSamples = VK_SAMPLE_COUNT_1_BIT;
		public static VkDevice device;
		private static VkQueue graphicsQueue;
		private VkQueue presentQueue;
		public static long swapChain;
		public static List<Long> swapChainImages;
		public static int swapChainImageFormat;
		public static VkExtent2D swapChainExtent;
		public static List<Long> swapChainImageViews;
		public static List<Long> swapChainFramebuffers;
		public static long renderPass;
		public static long descriptorPool;
		public static long descriptorSetLayout;
		private static List<Long> descriptorSets;
		public static long pipelineLayout;
		public static long graphicsPipeline;
		public static long commandPool;
		public static long colorImage;
		public static long colorImageMemory;
		public static long colorImageView;
		public static long depthImage;
		public static long depthImageMemory;
		public static long depthImageView;
		private int mipLevels;
		private long textureImage;
		private long textureImageMemory;
		private static long textureImageView;
		private static long textureSampler;
		private Vertex[] vertices;
		public static int[] indices;
		private static long vertexBuffer;
		private long vertexBufferMemory;
		private static long indexBuffer;
		private long indexBufferMemory;
		public static List<Long> uniformBuffers;
		public static List<Long> uniformBuffersMemory;
		public static List<VkCommandBuffer> commandBuffers;
		private List<Frame> inFlightFrames;
		private Map<Integer, Frame> imagesInFlight;
		private int currentFrame;
		boolean framebufferResize;
		// ======= METHODS ======= //

		public void run()
		{
			initWindow();
			initVulkan();
			mainLoop();
			cleanup();
		}

		private void initWindow()
		{
			Window.create(1200, 800, "Vulkan Ginger2", 60, RenderAPI.Vulkan);
			glfwSetFramebufferSizeCallback(Window.getWindow(), this::framebufferResizeCallback);
		}

		private void framebufferResizeCallback(long window, int width, int height)
		{
			// HelloTriangleApplication app = MemoryUtil.memGlobalRefToObject(glfwGetWindowUserPointer(window));
			// app.framebufferResize = true;
			framebufferResize = true;
		}

		private void initVulkan()
		{
			createInstance();
			createSurface();
			pickPhysicalDevice();
			createLogicalDevice();
			createCommandPool();
			createTextureImage();
			createTextureImageView();
			createTextureSampler();
			loadModel();
			createVertexBuffer();
			createIndexBuffer();
			createDescriptorSetLayout();
			VKSwapchainManager.createSwapChainObjects();
			createSyncObjects();
		}

		private void mainLoop()
		{
			while (!Window.closed())
			{
				if (Window.shouldRender())
				{ drawFrame(); }
				glfwPollEvents();
			}
			// Wait for the device to complete all operations before release resources
			vkDeviceWaitIdle(device);
		}

		private void cleanup()
		{
			VKSwapchainManager.cleanupSwapChain();
			vkDestroySampler(device, textureSampler, null);
			vkDestroyImageView(device, textureImageView, null);
			vkDestroyImage(device, textureImage, null);
			vkFreeMemory(device, textureImageMemory, null);
			vkDestroyDescriptorSetLayout(device, descriptorSetLayout, null);
			vkDestroyBuffer(device, indexBuffer, null);
			vkFreeMemory(device, indexBufferMemory, null);
			vkDestroyBuffer(device, vertexBuffer, null);
			vkFreeMemory(device, vertexBufferMemory, null);
			inFlightFrames.forEach(frame ->
			{
				vkDestroySemaphore(device, frame.renderFinishedSemaphore(), null);
				vkDestroySemaphore(device, frame.imageAvailableSemaphore(), null);
				vkDestroyFence(device, frame.fence(), null);
			});
			inFlightFrames.clear();
			vkDestroyCommandPool(device, commandPool, null);
			vkDestroyDevice(device, null);
			vkDestroySurfaceKHR(instance, surface, null);
			vkDestroyInstance(instance, null);
			glfwDestroyWindow(Window.getWindow());
			glfwTerminate();
		}

		private void createInstance()
		{
			try (MemoryStack stack = stackPush())
			{
				// Use calloc to initialize the structs with 0s. Otherwise, the program can crash due to random values
				VkApplicationInfo appInfo = VkApplicationInfo.callocStack(stack);
				appInfo.sType(VK_STRUCTURE_TYPE_APPLICATION_INFO);
				appInfo.pApplicationName(stack.UTF8Safe("Hello Triangle"));
				appInfo.applicationVersion(VK_MAKE_VERSION(1, 0, 0));
				appInfo.pEngineName(stack.UTF8Safe("No Engine"));
				appInfo.engineVersion(VK_MAKE_VERSION(1, 0, 0));
				appInfo.apiVersion(VK_API_VERSION_1_0);
				VkInstanceCreateInfo createInfo = VkInstanceCreateInfo.callocStack(stack);
				createInfo.sType(VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO);
				createInfo.pApplicationInfo(appInfo);
				// enabledExtensionCount is implicitly set when you call ppEnabledExtensionNames
				createInfo.ppEnabledExtensionNames(getRequiredExtensions());
				// We need to retrieve the pointer of the created instance
				PointerBuffer instancePtr = stack.mallocPointer(1);
				if (vkCreateInstance(createInfo, null, instancePtr) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create instance"); }
				instance = new VkInstance(instancePtr.get(0), createInfo);
			}
		}

		private void createSurface()
		{
			try (MemoryStack stack = stackPush())
			{
				LongBuffer pSurface = stack.longs(VK_NULL_HANDLE);
				if (glfwCreateWindowSurface(instance, Window.getWindow(), null, pSurface) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create window surface"); }
				surface = pSurface.get(0);
			}
		}

		private void pickPhysicalDevice()
		{
			try (MemoryStack stack = stackPush())
			{
				IntBuffer deviceCount = stack.ints(0);
				vkEnumeratePhysicalDevices(instance, deviceCount, null);
				if (deviceCount.get(0) == 0)
				{ throw new RuntimeException("Failed to find GPUs with Vulkan support"); }
				PointerBuffer ppPhysicalDevices = stack.mallocPointer(deviceCount.get(0));
				vkEnumeratePhysicalDevices(instance, deviceCount, ppPhysicalDevices);
				VkPhysicalDevice device = null;
				for (int i = 0; i < ppPhysicalDevices.capacity(); i++)
				{
					device = new VkPhysicalDevice(ppPhysicalDevices.get(i), instance);
					if (isDeviceSuitable(device))
					{ break; }
				}
				if (device == null)
				{ throw new RuntimeException("Failed to find a suitable GPU"); }
				physicalDevice = device;
				msaaSamples = getMaxUsableSampleCount();
			}
		}

		private void createLogicalDevice()
		{
			try (MemoryStack stack = stackPush())
			{
				QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
				int[] uniqueQueueFamilies = indices.unique();
				VkDeviceQueueCreateInfo.Buffer queueCreateInfos = VkDeviceQueueCreateInfo.callocStack(uniqueQueueFamilies.length, stack);
				for (int i = 0; i < uniqueQueueFamilies.length; i++)
				{
					VkDeviceQueueCreateInfo queueCreateInfo = queueCreateInfos.get(i);
					queueCreateInfo.sType(VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO);
					queueCreateInfo.queueFamilyIndex(uniqueQueueFamilies[i]);
					queueCreateInfo.pQueuePriorities(stack.floats(1.0f));
				}
				VkPhysicalDeviceFeatures deviceFeatures = VkPhysicalDeviceFeatures.callocStack(stack);
				deviceFeatures.samplerAnisotropy(true);
				deviceFeatures.sampleRateShading(true); // Enable sample shading feature for the device
				VkDeviceCreateInfo createInfo = VkDeviceCreateInfo.callocStack(stack);
				createInfo.sType(VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO);
				createInfo.pQueueCreateInfos(queueCreateInfos);
				// queueCreateInfoCount is automatically set
				createInfo.pEnabledFeatures(deviceFeatures);
				createInfo.ppEnabledExtensionNames(asPointerBuffer(DEVICE_EXTENSIONS));
				PointerBuffer pDevice = stack.pointers(VK_NULL_HANDLE);
				if (vkCreateDevice(physicalDevice, createInfo, null, pDevice) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create logical device"); }
				device = new VkDevice(pDevice.get(0), physicalDevice, createInfo);
				PointerBuffer pQueue = stack.pointers(VK_NULL_HANDLE);
				vkGetDeviceQueue(device, indices.graphicsFamily, 0, pQueue);
				graphicsQueue = new VkQueue(pQueue.get(0), device);
				vkGetDeviceQueue(device, indices.presentFamily, 0, pQueue);
				presentQueue = new VkQueue(pQueue.get(0), device);
			}
		}

		public static void createImageViews()
		{
			swapChainImageViews = new ArrayList<>(swapChainImages.size());
			for (long swapChainImage : swapChainImages)
			{ swapChainImageViews.add(createImageView(swapChainImage, swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1)); }
		}

		public static void createRenderPass()
		{
			try (MemoryStack stack = stackPush())
			{
				VkAttachmentDescription.Buffer attachments = VkAttachmentDescription.callocStack(3, stack);
				VkAttachmentReference.Buffer attachmentRefs = VkAttachmentReference.callocStack(3, stack);
				// Color attachments
				// MSAA Image
				VkAttachmentDescription colorAttachment = attachments.get(0);
				colorAttachment.format(swapChainImageFormat);
				colorAttachment.samples(msaaSamples);
				colorAttachment.loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR);
				colorAttachment.storeOp(VK_ATTACHMENT_STORE_OP_STORE);
				colorAttachment.stencilLoadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE);
				colorAttachment.stencilStoreOp(VK_ATTACHMENT_STORE_OP_DONT_CARE);
				colorAttachment.initialLayout(VK_IMAGE_LAYOUT_UNDEFINED);
				colorAttachment.finalLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
				VkAttachmentReference colorAttachmentRef = attachmentRefs.get(0);
				colorAttachmentRef.attachment(0);
				colorAttachmentRef.layout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
				// Present Image
				VkAttachmentDescription colorAttachmentResolve = attachments.get(2);
				colorAttachmentResolve.format(swapChainImageFormat);
				colorAttachmentResolve.samples(VK_SAMPLE_COUNT_1_BIT);
				colorAttachmentResolve.loadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE);
				colorAttachmentResolve.storeOp(VK_ATTACHMENT_STORE_OP_STORE);
				colorAttachmentResolve.stencilLoadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE);
				colorAttachmentResolve.stencilStoreOp(VK_ATTACHMENT_STORE_OP_DONT_CARE);
				colorAttachmentResolve.initialLayout(VK_IMAGE_LAYOUT_UNDEFINED);
				colorAttachmentResolve.finalLayout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
				VkAttachmentReference colorAttachmentResolveRef = attachmentRefs.get(2);
				colorAttachmentResolveRef.attachment(2);
				colorAttachmentResolveRef.layout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
				// Depth-Stencil attachments
				VkAttachmentDescription depthAttachment = attachments.get(1);
				depthAttachment.format(findDepthFormat());
				depthAttachment.samples(msaaSamples);
				depthAttachment.loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR);
				depthAttachment.storeOp(VK_ATTACHMENT_STORE_OP_DONT_CARE);
				depthAttachment.stencilLoadOp(VK_ATTACHMENT_LOAD_OP_DONT_CARE);
				depthAttachment.stencilStoreOp(VK_ATTACHMENT_STORE_OP_DONT_CARE);
				depthAttachment.initialLayout(VK_IMAGE_LAYOUT_UNDEFINED);
				depthAttachment.finalLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
				VkAttachmentReference depthAttachmentRef = attachmentRefs.get(1);
				depthAttachmentRef.attachment(1);
				depthAttachmentRef.layout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
				VkSubpassDescription.Buffer subpass = VkSubpassDescription.callocStack(1, stack);
				subpass.pipelineBindPoint(VK_PIPELINE_BIND_POINT_GRAPHICS);
				subpass.colorAttachmentCount(1);
				subpass.pColorAttachments(VkAttachmentReference.callocStack(1, stack).put(0, colorAttachmentRef));
				subpass.pDepthStencilAttachment(depthAttachmentRef);
				subpass.pResolveAttachments(VkAttachmentReference.callocStack(1, stack).put(0, colorAttachmentResolveRef));
				VkSubpassDependency.Buffer dependency = VkSubpassDependency.callocStack(1, stack);
				dependency.srcSubpass(VK_SUBPASS_EXTERNAL);
				dependency.dstSubpass(0);
				dependency.srcStageMask(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
				dependency.srcAccessMask(0);
				dependency.dstStageMask(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
				dependency.dstAccessMask(VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
				VkRenderPassCreateInfo renderPassInfo = VkRenderPassCreateInfo.callocStack(stack);
				renderPassInfo.sType(VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO);
				renderPassInfo.pAttachments(attachments);
				renderPassInfo.pSubpasses(subpass);
				renderPassInfo.pDependencies(dependency);
				LongBuffer pRenderPass = stack.mallocLong(1);
				if (vkCreateRenderPass(device, renderPassInfo, null, pRenderPass) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create render pass"); }
				renderPass = pRenderPass.get(0);
			}
		}

		private void createDescriptorSetLayout()
		{
			try (MemoryStack stack = stackPush())
			{
				VkDescriptorSetLayoutBinding.Buffer bindings = VkDescriptorSetLayoutBinding.callocStack(2, stack);
				VkDescriptorSetLayoutBinding uboLayoutBinding = bindings.get(0);
				uboLayoutBinding.binding(0);
				uboLayoutBinding.descriptorCount(1);
				uboLayoutBinding.descriptorType(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
				uboLayoutBinding.pImmutableSamplers(null);
				uboLayoutBinding.stageFlags(VK_SHADER_STAGE_VERTEX_BIT);
				VkDescriptorSetLayoutBinding samplerLayoutBinding = bindings.get(1);
				samplerLayoutBinding.binding(1);
				samplerLayoutBinding.descriptorCount(1);
				samplerLayoutBinding.descriptorType(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
				samplerLayoutBinding.pImmutableSamplers(null);
				samplerLayoutBinding.stageFlags(VK_SHADER_STAGE_FRAGMENT_BIT);
				VkDescriptorSetLayoutCreateInfo layoutInfo = VkDescriptorSetLayoutCreateInfo.callocStack(stack);
				layoutInfo.sType(VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO);
				layoutInfo.pBindings(bindings);
				LongBuffer pDescriptorSetLayout = stack.mallocLong(1);
				if (vkCreateDescriptorSetLayout(device, layoutInfo, null, pDescriptorSetLayout) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create descriptor set layout"); }
				descriptorSetLayout = pDescriptorSetLayout.get(0);
			}
		}

		public static void createFramebuffers()
		{
			swapChainFramebuffers = new ArrayList<>(swapChainImageViews.size());
			try (MemoryStack stack = stackPush())
			{
				LongBuffer attachments = stack.longs(colorImageView, depthImageView, VK_NULL_HANDLE);
				LongBuffer pFramebuffer = stack.mallocLong(1);
				// Lets allocate the create info struct once and just update the pAttachments field each iteration
				VkFramebufferCreateInfo framebufferInfo = VkFramebufferCreateInfo.callocStack(stack);
				framebufferInfo.sType(VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO);
				framebufferInfo.renderPass(renderPass);
				framebufferInfo.width(swapChainExtent.width());
				framebufferInfo.height(swapChainExtent.height());
				framebufferInfo.layers(1);
				for (long imageView : swapChainImageViews)
				{
					attachments.put(2, imageView);
					framebufferInfo.pAttachments(attachments);
					if (vkCreateFramebuffer(device, framebufferInfo, null, pFramebuffer) != VK_SUCCESS)
					{ throw new RuntimeException("Failed to create framebuffer"); }
					swapChainFramebuffers.add(pFramebuffer.get(0));
				}
			}
		}

		private void createCommandPool()
		{
			try (MemoryStack stack = stackPush())
			{
				QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);
				VkCommandPoolCreateInfo poolInfo = VkCommandPoolCreateInfo.callocStack(stack);
				poolInfo.sType(VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO);
				poolInfo.queueFamilyIndex(queueFamilyIndices.graphicsFamily);
				LongBuffer pCommandPool = stack.mallocLong(1);
				if (vkCreateCommandPool(device, poolInfo, null, pCommandPool) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create command pool"); }
				commandPool = pCommandPool.get(0);
			}
		}

		public static void createColorResources()
		{
			try (MemoryStack stack = stackPush())
			{
				LongBuffer pColorImage = stack.mallocLong(1);
				LongBuffer pColorImageMemory = stack.mallocLong(1);
				createImage(swapChainExtent.width(), swapChainExtent.height(),
					1,
					msaaSamples,
					swapChainImageFormat,
					VK_IMAGE_TILING_OPTIMAL,
					VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
					VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
					pColorImage,
					pColorImageMemory);
				colorImage = pColorImage.get(0);
				colorImageMemory = pColorImageMemory.get(0);
				colorImageView = createImageView(colorImage, swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1);
				transitionImageLayout(colorImage, swapChainImageFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 1);
			}
		}

		public static void createDepthResources()
		{
			try (MemoryStack stack = stackPush())
			{
				int depthFormat = findDepthFormat();
				LongBuffer pDepthImage = stack.mallocLong(1);
				LongBuffer pDepthImageMemory = stack.mallocLong(1);
				createImage(
					swapChainExtent.width(), swapChainExtent.height(),
					1,
					msaaSamples,
					depthFormat,
					VK_IMAGE_TILING_OPTIMAL,
					VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
					VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
					pDepthImage,
					pDepthImageMemory);
				depthImage = pDepthImage.get(0);
				depthImageMemory = pDepthImageMemory.get(0);
				depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, 1);
				// Explicitly transitioning the depth image
				transitionImageLayout(depthImage, depthFormat,
					VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
					1);
			}
		}

		private static int findSupportedFormat(IntBuffer formatCandidates, int tiling, int features)
		{
			try (MemoryStack stack = stackPush())
			{
				VkFormatProperties props = VkFormatProperties.callocStack(stack);
				for (int i = 0; i < formatCandidates.capacity(); ++i)
				{
					int format = formatCandidates.get(i);
					vkGetPhysicalDeviceFormatProperties(physicalDevice, format, props);
					if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures() & features) == features)
					{
						return format;
					}
					else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures() & features) == features)
					{ return format; }
				}
			}
			throw new RuntimeException("Failed to find supported format");
		}

		private static int findDepthFormat()
		{ return findSupportedFormat(
			stackGet().ints(VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT),
			VK_IMAGE_TILING_OPTIMAL,
			VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT); }

		private static boolean hasStencilComponent(int format)
		{ return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT; }

		private double log2(double n)
		{ return Math.log(n) / Math.log(2); }

		private void createTextureImage()
		{
			try (MemoryStack stack = stackPush())
			{
				String filename = Paths.get(new URI(ClassLoader.getSystemClassLoader().getResource("textures/chalet.jpg").toExternalForm())).toString();
				IntBuffer pWidth = stack.mallocInt(1);
				IntBuffer pHeight = stack.mallocInt(1);
				IntBuffer pChannels = stack.mallocInt(1);
				ByteBuffer pixels = stbi_load(filename, pWidth, pHeight, pChannels, STBI_rgb_alpha);
				long imageSize = pWidth.get(0) * pHeight.get(0) * 4; // pChannels.get(0);
				mipLevels = (int) Math.floor(log2(Math.max(pWidth.get(0), pHeight.get(0)))) + 1;
				if (pixels == null)
				{ throw new RuntimeException("Failed to load texture image " + filename); }
				LongBuffer pStagingBuffer = stack.mallocLong(1);
				LongBuffer pStagingBufferMemory = stack.mallocLong(1);
				createBuffer(imageSize,
					VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
					VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
					pStagingBuffer,
					pStagingBufferMemory);
				PointerBuffer data = stack.mallocPointer(1);
				vkMapMemory(device, pStagingBufferMemory.get(0), 0, imageSize, 0, data);
				{
					memcpy(data.getByteBuffer(0, (int) imageSize), pixels, imageSize);
				}
				vkUnmapMemory(device, pStagingBufferMemory.get(0));
				stbi_image_free(pixels);
				LongBuffer pTextureImage = stack.mallocLong(1);
				LongBuffer pTextureImageMemory = stack.mallocLong(1);
				createImage(pWidth.get(0), pHeight.get(0),
					mipLevels,
					VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL,
					VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
					VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
					pTextureImage,
					pTextureImageMemory);
				textureImage = pTextureImage.get(0);
				textureImageMemory = pTextureImageMemory.get(0);
				transitionImageLayout(textureImage,
					VK_FORMAT_R8G8B8A8_SRGB,
					VK_IMAGE_LAYOUT_UNDEFINED,
					VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
					mipLevels);
				copyBufferToImage(pStagingBuffer.get(0), textureImage, pWidth.get(0), pHeight.get(0));
				// Transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps
				generateMipmaps(textureImage, VK_FORMAT_R8G8B8A8_SRGB, pWidth.get(0), pHeight.get(0), mipLevels);
				vkDestroyBuffer(device, pStagingBuffer.get(0), null);
				vkFreeMemory(device, pStagingBufferMemory.get(0), null);
			}
			catch (URISyntaxException e)
			{
				e.printStackTrace();
			}
		}

		private void generateMipmaps(long image, int imageFormat, int width, int height, int mipLevels)
		{
			try (MemoryStack stack = stackPush())
			{
				// Check if image format supports linear blitting
				VkFormatProperties formatProperties = VkFormatProperties.mallocStack(stack);
				vkGetPhysicalDeviceFormatProperties(physicalDevice, imageFormat, formatProperties);
				if ((formatProperties.optimalTilingFeatures() & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) == 0)
				{ throw new RuntimeException("Texture image format does not support linear blitting"); }
				VkCommandBuffer commandBuffer = beginSingleTimeCommands();
				VkImageMemoryBarrier.Buffer barrier = VkImageMemoryBarrier.callocStack(1, stack);
				barrier.sType(VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER);
				barrier.image(image);
				barrier.srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED);
				barrier.dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED);
				barrier.dstAccessMask(VK_QUEUE_FAMILY_IGNORED);
				barrier.subresourceRange().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT);
				barrier.subresourceRange().baseArrayLayer(0);
				barrier.subresourceRange().layerCount(1);
				barrier.subresourceRange().levelCount(1);
				int mipWidth = width;
				int mipHeight = height;
				for (int i = 1; i < mipLevels; i++)
				{
					barrier.subresourceRange().baseMipLevel(i - 1);
					barrier.oldLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
					barrier.newLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
					barrier.srcAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT);
					barrier.dstAccessMask(VK_ACCESS_TRANSFER_READ_BIT);
					vkCmdPipelineBarrier(commandBuffer,
						VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
						null,
						null,
						barrier);
					VkImageBlit.Buffer blit = VkImageBlit.callocStack(1, stack);
					blit.srcOffsets(0).set(0, 0, 0);
					blit.srcOffsets(1).set(mipWidth, mipHeight, 1);
					blit.srcSubresource().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT);
					blit.srcSubresource().mipLevel(i - 1);
					blit.srcSubresource().baseArrayLayer(0);
					blit.srcSubresource().layerCount(1);
					blit.dstOffsets(0).set(0, 0, 0);
					blit.dstOffsets(1).set(mipWidth > 1 ? mipWidth / 2 : 1, mipHeight > 1 ? mipHeight / 2 : 1, 1);
					blit.dstSubresource().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT);
					blit.dstSubresource().mipLevel(i);
					blit.dstSubresource().baseArrayLayer(0);
					blit.dstSubresource().layerCount(1);
					vkCmdBlitImage(commandBuffer,
						image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
						image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
						blit,
						VK_FILTER_LINEAR);
					barrier.oldLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
					barrier.newLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
					barrier.srcAccessMask(VK_ACCESS_TRANSFER_READ_BIT);
					barrier.dstAccessMask(VK_ACCESS_SHADER_READ_BIT);
					vkCmdPipelineBarrier(commandBuffer,
						VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
						null,
						null,
						barrier);
					if (mipWidth > 1)
					{ mipWidth /= 2; }
					if (mipHeight > 1)
					{ mipHeight /= 2; }
				}
				barrier.subresourceRange().baseMipLevel(mipLevels - 1);
				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,
					null,
					null,
					barrier);
				endSingleTimeCommands(commandBuffer);
			}
		}

		private int getMaxUsableSampleCount()
		{
			try (MemoryStack stack = stackPush())
			{
				VkPhysicalDeviceProperties physicalDeviceProperties = VkPhysicalDeviceProperties.mallocStack(stack);
				vkGetPhysicalDeviceProperties(physicalDevice, physicalDeviceProperties);
				int sampleCountFlags = physicalDeviceProperties.limits().framebufferColorSampleCounts()
					& physicalDeviceProperties.limits().framebufferDepthSampleCounts();
				if ((sampleCountFlags & VK_SAMPLE_COUNT_64_BIT) != 0)
				{ return VK_SAMPLE_COUNT_64_BIT; }
				if ((sampleCountFlags & VK_SAMPLE_COUNT_32_BIT) != 0)
				{ return VK_SAMPLE_COUNT_32_BIT; }
				if ((sampleCountFlags & VK_SAMPLE_COUNT_16_BIT) != 0)
				{ return VK_SAMPLE_COUNT_16_BIT; }
				if ((sampleCountFlags & VK_SAMPLE_COUNT_8_BIT) != 0)
				{ return VK_SAMPLE_COUNT_8_BIT; }
				if ((sampleCountFlags & VK_SAMPLE_COUNT_4_BIT) != 0)
				{ return VK_SAMPLE_COUNT_4_BIT; }
				if ((sampleCountFlags & VK_SAMPLE_COUNT_2_BIT) != 0)
				{ return VK_SAMPLE_COUNT_2_BIT; }
				return VK_SAMPLE_COUNT_1_BIT;
			}
		}

		private void createTextureImageView()
		{ textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels); }

		private void createTextureSampler()
		{
			try (MemoryStack stack = stackPush())
			{
				VkSamplerCreateInfo samplerInfo = VkSamplerCreateInfo.callocStack(stack);
				samplerInfo.sType(VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO);
				samplerInfo.magFilter(VK_FILTER_LINEAR);
				samplerInfo.minFilter(VK_FILTER_LINEAR);
				samplerInfo.addressModeU(VK_SAMPLER_ADDRESS_MODE_REPEAT);
				samplerInfo.addressModeV(VK_SAMPLER_ADDRESS_MODE_REPEAT);
				samplerInfo.addressModeW(VK_SAMPLER_ADDRESS_MODE_REPEAT);
				samplerInfo.anisotropyEnable(true);
				samplerInfo.maxAnisotropy(16.0f);
				samplerInfo.borderColor(VK_BORDER_COLOR_INT_OPAQUE_BLACK);
				samplerInfo.unnormalizedCoordinates(false);
				samplerInfo.compareEnable(false);
				samplerInfo.compareOp(VK_COMPARE_OP_ALWAYS);
				samplerInfo.mipmapMode(VK_SAMPLER_MIPMAP_MODE_LINEAR);
				samplerInfo.minLod(0); // Optional
				samplerInfo.maxLod(mipLevels);
				samplerInfo.mipLodBias(0); // Optional
				LongBuffer pTextureSampler = stack.mallocLong(1);
				if (vkCreateSampler(device, samplerInfo, null, pTextureSampler) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create texture sampler"); }
				textureSampler = pTextureSampler.get(0);
			}
		}

		private static long createImageView(long image, int format, int aspectFlags, int mipLevels)
		{
			try (MemoryStack stack = stackPush())
			{
				VkImageViewCreateInfo viewInfo = VkImageViewCreateInfo.callocStack(stack);
				viewInfo.sType(VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO);
				viewInfo.image(image);
				viewInfo.viewType(VK_IMAGE_VIEW_TYPE_2D);
				viewInfo.format(format);
				viewInfo.subresourceRange().aspectMask(aspectFlags);
				viewInfo.subresourceRange().baseMipLevel(0);
				viewInfo.subresourceRange().levelCount(mipLevels);
				viewInfo.subresourceRange().baseArrayLayer(0);
				viewInfo.subresourceRange().layerCount(1);
				LongBuffer pImageView = stack.mallocLong(1);
				if (vkCreateImageView(device, viewInfo, null, pImageView) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create texture image view"); }
				return pImageView.get(0);
			}
		}

		private static void createImage(int width, int height, int mipLevels, int numSamples, int format, int tiling, int usage, int memProperties,
			LongBuffer pTextureImage, LongBuffer pTextureImageMemory)
		{
			try (MemoryStack stack = stackPush())
			{
				VkImageCreateInfo imageInfo = VkImageCreateInfo.callocStack(stack);
				imageInfo.sType(VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO);
				imageInfo.imageType(VK_IMAGE_TYPE_2D);
				imageInfo.extent().width(width);
				imageInfo.extent().height(height);
				imageInfo.extent().depth(1);
				imageInfo.mipLevels(mipLevels);
				imageInfo.arrayLayers(1);
				imageInfo.format(format);
				imageInfo.tiling(tiling);
				imageInfo.initialLayout(VK_IMAGE_LAYOUT_UNDEFINED);
				imageInfo.usage(usage);
				imageInfo.samples(numSamples);
				imageInfo.sharingMode(VK_SHARING_MODE_EXCLUSIVE);
				if (vkCreateImage(device, imageInfo, null, pTextureImage) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create image"); }
				VkMemoryRequirements memRequirements = VkMemoryRequirements.mallocStack(stack);
				vkGetImageMemoryRequirements(device, pTextureImage.get(0), memRequirements);
				VkMemoryAllocateInfo allocInfo = VkMemoryAllocateInfo.callocStack(stack);
				allocInfo.sType(VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO);
				allocInfo.allocationSize(memRequirements.size());
				allocInfo.memoryTypeIndex(findMemoryType(memRequirements.memoryTypeBits(), memProperties));
				if (vkAllocateMemory(device, allocInfo, null, pTextureImageMemory) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to allocate image memory"); }
				vkBindImageMemory(device, pTextureImage.get(0), pTextureImageMemory.get(0), 0);
			}
		}

		private static void transitionImageLayout(long image, int format, int oldLayout, int newLayout, int mipLevels)
		{
			try (MemoryStack stack = stackPush())
			{
				VkImageMemoryBarrier.Buffer barrier = VkImageMemoryBarrier.callocStack(1, stack);
				barrier.sType(VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER);
				barrier.oldLayout(oldLayout);
				barrier.newLayout(newLayout);
				barrier.srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED);
				barrier.dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED);
				barrier.image(image);
				barrier.subresourceRange().baseMipLevel(0);
				barrier.subresourceRange().levelCount(mipLevels);
				barrier.subresourceRange().baseArrayLayer(0);
				barrier.subresourceRange().layerCount(1);
				if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
				{
					barrier.subresourceRange().aspectMask(VK_IMAGE_ASPECT_DEPTH_BIT);
					if (hasStencilComponent(format))
					{ barrier.subresourceRange().aspectMask(
						barrier.subresourceRange().aspectMask() | VK_IMAGE_ASPECT_STENCIL_BIT); }
				}
				else
				{
					barrier.subresourceRange().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT);
				}
				int sourceStage;
				int destinationStage;
				if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
				{
					barrier.srcAccessMask(0);
					barrier.dstAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT);
					sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
					destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
				}
				else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
				{
					barrier.srcAccessMask(VK_ACCESS_TRANSFER_WRITE_BIT);
					barrier.dstAccessMask(VK_ACCESS_SHADER_READ_BIT);
					sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
					destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
				}
				else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
				{
					barrier.srcAccessMask(0);
					barrier.dstAccessMask(VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT);
					sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
					destinationStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
				}
				else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)
				{
					barrier.srcAccessMask(0);
					barrier.dstAccessMask(VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
					sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
					destinationStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
				}
				else
				{
					throw new IllegalArgumentException("Unsupported layout transition");
				}
				VkCommandBuffer commandBuffer = beginSingleTimeCommands();
				vkCmdPipelineBarrier(commandBuffer,
					sourceStage, destinationStage,
					0,
					null,
					null,
					barrier);
				endSingleTimeCommands(commandBuffer);
			}
		}

		private void copyBufferToImage(long buffer, long image, int width, int height)
		{
			try (MemoryStack stack = stackPush())
			{
				VkCommandBuffer commandBuffer = beginSingleTimeCommands();
				VkBufferImageCopy.Buffer region = VkBufferImageCopy.callocStack(1, stack);
				region.bufferOffset(0);
				region.bufferRowLength(0); // Tightly packed
				region.bufferImageHeight(0); // Tightly packed
				region.imageSubresource().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT);
				region.imageSubresource().mipLevel(0);
				region.imageSubresource().baseArrayLayer(0);
				region.imageSubresource().layerCount(1);
				region.imageOffset().set(0, 0, 0);
				region.imageExtent(VkExtent3D.callocStack(stack).set(width, height, 1));
				vkCmdCopyBufferToImage(commandBuffer, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region);
				endSingleTimeCommands(commandBuffer);
			}
		}

		private void memcpy(ByteBuffer dst, ByteBuffer src, long size)
		{
			src.limit((int) size);
			dst.put(src);
			src.limit(src.capacity()).rewind();
		}

		private void loadModel()
		{
			File modelFile = new File(ClassLoader.getSystemClassLoader().getResource("models/chalet.obj").getFile());
			VKMesh model = VKModelLoader.loadModel(modelFile, aiProcess_FlipUVs | aiProcess_DropNormals);
			final int vertexCount = model.positions.size();
			vertices = new Vertex[vertexCount];
			final Vector3fc color = new Vector3f(1.0f, 1.0f, 1.0f);
			for (int i = 0; i < vertexCount; i++)
			{ vertices[i] = new Vertex(
				model.positions.get(i),
				color,
				model.texCoords.get(i)); }
			indices = new int[model.indices.size()];
			for (int i = 0; i < indices.length; i++)
			{ indices[i] = model.indices.get(i); }
		}

		private void createVertexBuffer()
		{
			try (MemoryStack stack = stackPush())
			{
				long bufferSize = Vertex.SIZEOF * vertices.length;
				LongBuffer pBuffer = stack.mallocLong(1);
				LongBuffer pBufferMemory = stack.mallocLong(1);
				createBuffer(bufferSize,
					VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
					VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
					pBuffer,
					pBufferMemory);
				long stagingBuffer = pBuffer.get(0);
				long stagingBufferMemory = pBufferMemory.get(0);
				PointerBuffer data = stack.mallocPointer(1);
				vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, data);
				{
					memcpy(data.getByteBuffer(0, (int) bufferSize), vertices);
				}
				vkUnmapMemory(device, stagingBufferMemory);
				createBuffer(bufferSize,
					VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
					VK_MEMORY_HEAP_DEVICE_LOCAL_BIT,
					pBuffer,
					pBufferMemory);
				vertexBuffer = pBuffer.get(0);
				vertexBufferMemory = pBufferMemory.get(0);
				copyBuffer(stagingBuffer, vertexBuffer, bufferSize);
				vkDestroyBuffer(device, stagingBuffer, null);
				vkFreeMemory(device, stagingBufferMemory, null);
			}
		}

		private void createIndexBuffer()
		{
			try (MemoryStack stack = stackPush())
			{
				long bufferSize = Integer.BYTES * indices.length;
				LongBuffer pBuffer = stack.mallocLong(1);
				LongBuffer pBufferMemory = stack.mallocLong(1);
				createBuffer(bufferSize,
					VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
					VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
					pBuffer,
					pBufferMemory);
				long stagingBuffer = pBuffer.get(0);
				long stagingBufferMemory = pBufferMemory.get(0);
				PointerBuffer data = stack.mallocPointer(1);
				vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, data);
				{
					memcpy(data.getByteBuffer(0, (int) bufferSize), indices);
				}
				vkUnmapMemory(device, stagingBufferMemory);
				createBuffer(bufferSize,
					VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
					VK_MEMORY_HEAP_DEVICE_LOCAL_BIT,
					pBuffer,
					pBufferMemory);
				indexBuffer = pBuffer.get(0);
				indexBufferMemory = pBufferMemory.get(0);
				copyBuffer(stagingBuffer, indexBuffer, bufferSize);
				vkDestroyBuffer(device, stagingBuffer, null);
				vkFreeMemory(device, stagingBufferMemory, null);
			}
		}

		public static void createUniformBuffers()
		{
			try (MemoryStack stack = stackPush())
			{
				uniformBuffers = new ArrayList<>(swapChainImages.size());
				uniformBuffersMemory = new ArrayList<>(swapChainImages.size());
				LongBuffer pBuffer = stack.mallocLong(1);
				LongBuffer pBufferMemory = stack.mallocLong(1);
				for (int i = 0; i < swapChainImages.size(); i++)
				{
					createBuffer(UniformBufferObject.SIZEOF,
						VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
						VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
						pBuffer,
						pBufferMemory);
					uniformBuffers.add(pBuffer.get(0));
					uniformBuffersMemory.add(pBufferMemory.get(0));
				}
			}
		}

		public static void createDescriptorPool()
		{
			try (MemoryStack stack = stackPush())
			{
				VkDescriptorPoolSize.Buffer poolSizes = VkDescriptorPoolSize.callocStack(2, stack);
				VkDescriptorPoolSize uniformBufferPoolSize = poolSizes.get(0);
				uniformBufferPoolSize.type(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
				uniformBufferPoolSize.descriptorCount(swapChainImages.size());
				VkDescriptorPoolSize textureSamplerPoolSize = poolSizes.get(1);
				textureSamplerPoolSize.type(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
				textureSamplerPoolSize.descriptorCount(swapChainImages.size());
				VkDescriptorPoolCreateInfo poolInfo = VkDescriptorPoolCreateInfo.callocStack(stack);
				poolInfo.sType(VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO);
				poolInfo.pPoolSizes(poolSizes);
				poolInfo.maxSets(swapChainImages.size());
				LongBuffer pDescriptorPool = stack.mallocLong(1);
				if (vkCreateDescriptorPool(device, poolInfo, null, pDescriptorPool) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create descriptor pool"); }
				descriptorPool = pDescriptorPool.get(0);
			}
		}

		public static void createDescriptorSets()
		{
			try (MemoryStack stack = stackPush())
			{
				LongBuffer layouts = stack.mallocLong(swapChainImages.size());
				for (int i = 0; i < layouts.capacity(); i++)
				{ layouts.put(i, descriptorSetLayout); }
				VkDescriptorSetAllocateInfo allocInfo = VkDescriptorSetAllocateInfo.callocStack(stack);
				allocInfo.sType(VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO);
				allocInfo.descriptorPool(descriptorPool);
				allocInfo.pSetLayouts(layouts);
				LongBuffer pDescriptorSets = stack.mallocLong(swapChainImages.size());
				if (vkAllocateDescriptorSets(device, allocInfo, pDescriptorSets) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to allocate descriptor sets"); }
				descriptorSets = new ArrayList<>(pDescriptorSets.capacity());
				VkDescriptorBufferInfo.Buffer bufferInfo = VkDescriptorBufferInfo.callocStack(1, stack);
				bufferInfo.offset(0);
				bufferInfo.range(UniformBufferObject.SIZEOF);
				VkDescriptorImageInfo.Buffer imageInfo = VkDescriptorImageInfo.callocStack(1, stack);
				imageInfo.imageLayout(VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
				imageInfo.imageView(textureImageView);
				imageInfo.sampler(textureSampler);
				VkWriteDescriptorSet.Buffer descriptorWrites = VkWriteDescriptorSet.callocStack(2, stack);
				VkWriteDescriptorSet uboDescriptorWrite = descriptorWrites.get(0);
				uboDescriptorWrite.sType(VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET);
				uboDescriptorWrite.dstBinding(0);
				uboDescriptorWrite.dstArrayElement(0);
				uboDescriptorWrite.descriptorType(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
				uboDescriptorWrite.descriptorCount(1);
				uboDescriptorWrite.pBufferInfo(bufferInfo);
				VkWriteDescriptorSet samplerDescriptorWrite = descriptorWrites.get(1);
				samplerDescriptorWrite.sType(VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET);
				samplerDescriptorWrite.dstBinding(1);
				samplerDescriptorWrite.dstArrayElement(0);
				samplerDescriptorWrite.descriptorType(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
				samplerDescriptorWrite.descriptorCount(1);
				samplerDescriptorWrite.pImageInfo(imageInfo);
				for (int i = 0; i < pDescriptorSets.capacity(); i++)
				{
					long descriptorSet = pDescriptorSets.get(i);
					bufferInfo.buffer(uniformBuffers.get(i));
					uboDescriptorWrite.dstSet(descriptorSet);
					samplerDescriptorWrite.dstSet(descriptorSet);
					vkUpdateDescriptorSets(device, descriptorWrites, null);
					descriptorSets.add(descriptorSet);
				}
			}
		}

		private static void createBuffer(long size, int usage, int properties, LongBuffer pBuffer, LongBuffer pBufferMemory)
		{
			try (MemoryStack stack = stackPush())
			{
				VkBufferCreateInfo bufferInfo = VkBufferCreateInfo.callocStack(stack);
				bufferInfo.sType(VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO);
				bufferInfo.size(size);
				bufferInfo.usage(usage);
				bufferInfo.sharingMode(VK_SHARING_MODE_EXCLUSIVE);
				if (vkCreateBuffer(device, bufferInfo, null, pBuffer) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to create vertex buffer"); }
				VkMemoryRequirements memRequirements = VkMemoryRequirements.mallocStack(stack);
				vkGetBufferMemoryRequirements(device, pBuffer.get(0), memRequirements);
				VkMemoryAllocateInfo allocInfo = VkMemoryAllocateInfo.callocStack(stack);
				allocInfo.sType(VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO);
				allocInfo.allocationSize(memRequirements.size());
				allocInfo.memoryTypeIndex(findMemoryType(memRequirements.memoryTypeBits(), properties));
				if (vkAllocateMemory(device, allocInfo, null, pBufferMemory) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to allocate vertex buffer memory"); }
				vkBindBufferMemory(device, pBuffer.get(0), pBufferMemory.get(0), 0);
			}
		}

		private static VkCommandBuffer beginSingleTimeCommands()
		{
			try (MemoryStack stack = stackPush())
			{
				VkCommandBufferAllocateInfo allocInfo = VkCommandBufferAllocateInfo.callocStack(stack);
				allocInfo.sType(VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO);
				allocInfo.level(VK_COMMAND_BUFFER_LEVEL_PRIMARY);
				allocInfo.commandPool(commandPool);
				allocInfo.commandBufferCount(1);
				PointerBuffer pCommandBuffer = stack.mallocPointer(1);
				vkAllocateCommandBuffers(device, allocInfo, pCommandBuffer);
				VkCommandBuffer commandBuffer = new VkCommandBuffer(pCommandBuffer.get(0), device);
				VkCommandBufferBeginInfo beginInfo = VkCommandBufferBeginInfo.callocStack(stack);
				beginInfo.sType(VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO);
				beginInfo.flags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
				vkBeginCommandBuffer(commandBuffer, beginInfo);
				return commandBuffer;
			}
		}

		private static void endSingleTimeCommands(VkCommandBuffer commandBuffer)
		{
			try (MemoryStack stack = stackPush())
			{
				vkEndCommandBuffer(commandBuffer);
				VkSubmitInfo.Buffer submitInfo = VkSubmitInfo.callocStack(1, stack);
				submitInfo.sType(VK_STRUCTURE_TYPE_SUBMIT_INFO);
				submitInfo.pCommandBuffers(stack.pointers(commandBuffer));
				vkQueueSubmit(graphicsQueue, submitInfo, VK_NULL_HANDLE);
				vkQueueWaitIdle(graphicsQueue);
				vkFreeCommandBuffers(device, commandPool, commandBuffer);
			}
		}

		private void copyBuffer(long srcBuffer, long dstBuffer, long size)
		{
			try (MemoryStack stack = stackPush())
			{
				VkCommandBuffer commandBuffer = beginSingleTimeCommands();
				VkBufferCopy.Buffer copyRegion = VkBufferCopy.callocStack(1, stack);
				copyRegion.size(size);
				vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, copyRegion);
				endSingleTimeCommands(commandBuffer);
			}
		}

		private void memcpy(ByteBuffer buffer, Vertex[] vertices)
		{
			for (Vertex vertex : vertices)
			{
				buffer.putFloat(vertex.pos.x());
				buffer.putFloat(vertex.pos.y());
				buffer.putFloat(vertex.pos.z());
				buffer.putFloat(vertex.color.x());
				buffer.putFloat(vertex.color.y());
				buffer.putFloat(vertex.color.z());
				buffer.putFloat(vertex.texCoords.x());
				buffer.putFloat(vertex.texCoords.y());
			}
		}

		private void memcpy(ByteBuffer buffer, int[] indices)
		{
			for (int index : indices)
			{ buffer.putInt(index); }
			buffer.rewind();
		}

		private void memcpy(ByteBuffer buffer, UniformBufferObject ubo)
		{
			final int mat4Size = 16 * Float.BYTES;
			ubo.model.get(0, buffer);
			ubo.view.get(AlignmentUtils.alignas(mat4Size, AlignmentUtils.alignof(ubo.view)), buffer);
			ubo.proj.get(AlignmentUtils.alignas(mat4Size * 2, AlignmentUtils.alignof(ubo.view)), buffer);
		}

		private static int findMemoryType(int typeFilter, int properties)
		{
			VkPhysicalDeviceMemoryProperties memProperties = VkPhysicalDeviceMemoryProperties.mallocStack();
			vkGetPhysicalDeviceMemoryProperties(physicalDevice, memProperties);
			for (int i = 0; i < memProperties.memoryTypeCount(); i++)
			{
				if ((typeFilter & (1 << i)) != 0 && (memProperties.memoryTypes(i).propertyFlags() & properties) == properties)
				{ return i; }
			}
			throw new RuntimeException("Failed to find suitable memory type");
		}

		public static void createCommandBuffers()
		{
			final int commandBuffersCount = swapChainFramebuffers.size();
			commandBuffers = new ArrayList<>(commandBuffersCount);
			try (MemoryStack stack = stackPush())
			{
				VkCommandBufferAllocateInfo allocInfo = VkCommandBufferAllocateInfo.callocStack(stack);
				allocInfo.sType(VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO);
				allocInfo.commandPool(commandPool);
				allocInfo.level(VK_COMMAND_BUFFER_LEVEL_PRIMARY);
				allocInfo.commandBufferCount(commandBuffersCount);
				PointerBuffer pCommandBuffers = stack.mallocPointer(commandBuffersCount);
				if (vkAllocateCommandBuffers(device, allocInfo, pCommandBuffers) != VK_SUCCESS)
				{ throw new RuntimeException("Failed to allocate command buffers"); }
				for (int i = 0; i < commandBuffersCount; i++)
				{ commandBuffers.add(new VkCommandBuffer(pCommandBuffers.get(i), device)); }
				VkCommandBufferBeginInfo beginInfo = VkCommandBufferBeginInfo.callocStack(stack);
				beginInfo.sType(VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO);
				VkRenderPassBeginInfo renderPassInfo = VkRenderPassBeginInfo.callocStack(stack);
				renderPassInfo.sType(VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO);
				renderPassInfo.renderPass(renderPass);
				VkRect2D renderArea = VkRect2D.callocStack(stack);
				renderArea.offset(VkOffset2D.callocStack(stack).set(0, 0));
				renderArea.extent(swapChainExtent);
				renderPassInfo.renderArea(renderArea);
				VkClearValue.Buffer clearValues = VkClearValue.callocStack(2, stack);
				clearValues.get(0).color().float32(stack.floats(Window.getColour().x / 255, Window.getColour().y / 255, Window.getColour().z / 255, 1.0f)); //The screens clear colour
				clearValues.get(1).depthStencil().set(1.0f, 0);
				renderPassInfo.pClearValues(clearValues);
				for (int i = 0; i < commandBuffersCount; i++)
				{
					VkCommandBuffer commandBuffer = commandBuffers.get(i);
					if (vkBeginCommandBuffer(commandBuffer, beginInfo) != VK_SUCCESS)
					{ throw new RuntimeException("Failed to begin recording command buffer"); }
					renderPassInfo.framebuffer(swapChainFramebuffers.get(i));
					vkCmdBeginRenderPass(commandBuffer, renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
					{
						vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
						LongBuffer vertexBuffers = stack.longs(vertexBuffer);
						LongBuffer offsets = stack.longs(0);
						vkCmdBindVertexBuffers(commandBuffer, 0, vertexBuffers, offsets);
						vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
						vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
							pipelineLayout,
							0, stack.longs(
								descriptorSets.get(i)),
							null);
						vkCmdDrawIndexed(commandBuffer, indices.length, 1, 0, 0, 0);
					}
					vkCmdEndRenderPass(commandBuffer);
					if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS)
					{ throw new RuntimeException("Failed to record command buffer"); }
				}
			}
		}

		private void createSyncObjects()
		{
			inFlightFrames = new ArrayList<>(MAX_FRAMES_IN_FLIGHT);
			imagesInFlight = new HashMap<>(swapChainImages.size());
			try (MemoryStack stack = stackPush())
			{
				VkSemaphoreCreateInfo semaphoreInfo = VkSemaphoreCreateInfo.callocStack(stack);
				semaphoreInfo.sType(VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO);
				VkFenceCreateInfo fenceInfo = VkFenceCreateInfo.callocStack(stack);
				fenceInfo.sType(VK_STRUCTURE_TYPE_FENCE_CREATE_INFO);
				fenceInfo.flags(VK_FENCE_CREATE_SIGNALED_BIT);
				LongBuffer pImageAvailableSemaphore = stack.mallocLong(1);
				LongBuffer pRenderFinishedSemaphore = stack.mallocLong(1);
				LongBuffer pFence = stack.mallocLong(1);
				for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; i++)
				{
					if (vkCreateSemaphore(device, semaphoreInfo, null, pImageAvailableSemaphore) != VK_SUCCESS
						|| vkCreateSemaphore(device, semaphoreInfo, null, pRenderFinishedSemaphore) != VK_SUCCESS
						|| vkCreateFence(device, fenceInfo, null, pFence) != VK_SUCCESS)
					{ throw new RuntimeException("Failed to create synchronization objects for the frame " + i); }
					inFlightFrames.add(new Frame(pImageAvailableSemaphore.get(0), pRenderFinishedSemaphore.get(0), pFence.get(0)));
				}
			}
		}

		private void updateUniformBuffer(int currentImage)
		{
			try (MemoryStack stack = stackPush())
			{
				UniformBufferObject ubo = new UniformBufferObject();
				ubo.model.rotate((float) (glfwGetTime() * Math.toRadians(90)), 0.0f, 0.0f, 1.0f);
				ubo.view.lookAt(2.0f, 2.0f, 2.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f);
				ubo.proj.perspective((float) Math.toRadians(45),
					(float) swapChainExtent.width() / (float) swapChainExtent.height(), 0.1f, 10.0f);
				ubo.proj.m11(ubo.proj.m11() * -1);
				PointerBuffer data = stack.mallocPointer(1);
				vkMapMemory(device, uniformBuffersMemory.get(currentImage), 0, UniformBufferObject.SIZEOF, 0, data);
				{
					memcpy(data.getByteBuffer(0, UniformBufferObject.SIZEOF), ubo);
				}
				vkUnmapMemory(device, uniformBuffersMemory.get(currentImage));
			}
		}

		private void drawFrame()
		{
			try (MemoryStack stack = stackPush())
			{
				Frame thisFrame = inFlightFrames.get(currentFrame);
				vkWaitForFences(device, thisFrame.pFence(), true, UINT64_MAX);
				IntBuffer pImageIndex = stack.mallocInt(1);
				int vkResult = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX,
					thisFrame.imageAvailableSemaphore(), VK_NULL_HANDLE, pImageIndex);
				if (vkResult == VK_ERROR_OUT_OF_DATE_KHR)
				{
					VKSwapchainManager.recreateSwapChain();
					return;
				}
				else if (vkResult != VK_SUCCESS)
				{ throw new RuntimeException("Cannot get image"); }
				final int imageIndex = pImageIndex.get(0);
				updateUniformBuffer(imageIndex);
				if (imagesInFlight.containsKey(imageIndex))
				{ vkWaitForFences(device, imagesInFlight.get(imageIndex).fence(), true, UINT64_MAX); }
				imagesInFlight.put(imageIndex, thisFrame);
				VkSubmitInfo submitInfo = VkSubmitInfo.callocStack(stack);
				submitInfo.sType(VK_STRUCTURE_TYPE_SUBMIT_INFO);
				submitInfo.waitSemaphoreCount(1);
				submitInfo.pWaitSemaphores(thisFrame.pImageAvailableSemaphore());
				submitInfo.pWaitDstStageMask(stack.ints(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT));
				submitInfo.pSignalSemaphores(thisFrame.pRenderFinishedSemaphore());
				submitInfo.pCommandBuffers(stack.pointers(commandBuffers.get(imageIndex)));
				vkResetFences(device, thisFrame.pFence());
				if ((vkResult = vkQueueSubmit(graphicsQueue, submitInfo, thisFrame.fence())) != VK_SUCCESS)
				{
					vkResetFences(device, thisFrame.pFence());
					throw new RuntimeException("Failed to submit draw command buffer: " + vkResult);
				}
				VkPresentInfoKHR presentInfo = VkPresentInfoKHR.callocStack(stack);
				presentInfo.sType(VK_STRUCTURE_TYPE_PRESENT_INFO_KHR);
				presentInfo.pWaitSemaphores(thisFrame.pRenderFinishedSemaphore());
				presentInfo.swapchainCount(1);
				presentInfo.pSwapchains(stack.longs(swapChain));
				presentInfo.pImageIndices(pImageIndex);
				vkResult = vkQueuePresentKHR(presentQueue, presentInfo);
				if (vkResult == VK_ERROR_OUT_OF_DATE_KHR || vkResult == VK_SUBOPTIMAL_KHR || framebufferResize)
				{
					framebufferResize = false;
					VKSwapchainManager.recreateSwapChain();
				}
				else if (vkResult != VK_SUCCESS)
				{ throw new RuntimeException("Failed to present swap chain image"); }
				currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
			}
		}

		public static VkSurfaceFormatKHR chooseSwapSurfaceFormat(VkSurfaceFormatKHR.Buffer availableFormats)
		{ return availableFormats.stream()
			.filter(availableFormat -> availableFormat.format() == VK_FORMAT_B8G8R8_SRGB)
			.filter(availableFormat -> availableFormat.colorSpace() == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
			.findAny()
			.orElse(availableFormats.get(0)); }

		public static int chooseSwapPresentMode(IntBuffer availablePresentModes)
		{
			for (int i = 0; i < availablePresentModes.capacity(); i++)
			{
				if (availablePresentModes.get(i) == VK_PRESENT_MODE_MAILBOX_KHR)
				{ return availablePresentModes.get(i); }
			}
			return VK_PRESENT_MODE_FIFO_KHR;
		}

		public static VkExtent2D chooseSwapExtent(VkSurfaceCapabilitiesKHR capabilities)
		{
			if (capabilities.currentExtent().width() != UINT32_MAX)
			{ return capabilities.currentExtent(); }
			IntBuffer width = stackGet().ints(0);
			IntBuffer height = stackGet().ints(0);
			glfwGetFramebufferSize(Window.getWindow(), width, height);
			VkExtent2D actualExtent = VkExtent2D.mallocStack().set(width.get(0), height.get(0));
			VkExtent2D minExtent = capabilities.minImageExtent();
			VkExtent2D maxExtent = capabilities.maxImageExtent();
			actualExtent.width(clamp(minExtent.width(), maxExtent.width(), actualExtent.width()));
			actualExtent.height(clamp(minExtent.height(), maxExtent.height(), actualExtent.height()));
			return actualExtent;
		}

		private static int clamp(int min, int max, int value)
		{ return Math.max(min, Math.min(max, value)); }

		private boolean isDeviceSuitable(VkPhysicalDevice device)
		{
			QueueFamilyIndices indices = findQueueFamilies(device);
			boolean extensionsSupported = checkDeviceExtensionSupport(device);
			boolean swapChainAdequate = false;
			boolean anisotropySupported = false;
			if (extensionsSupported)
			{
				try (MemoryStack stack = stackPush())
				{
					SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device, stack);
					swapChainAdequate = swapChainSupport.formats.hasRemaining() && swapChainSupport.presentModes.hasRemaining();
					VkPhysicalDeviceFeatures supportedFeatures = VkPhysicalDeviceFeatures.mallocStack(stack);
					vkGetPhysicalDeviceFeatures(device, supportedFeatures);
					anisotropySupported = supportedFeatures.samplerAnisotropy();
				}
			}
			return indices.isComplete() && extensionsSupported && swapChainAdequate && anisotropySupported;
		}

		@SuppressWarnings("unlikely-arg-type")
		private boolean checkDeviceExtensionSupport(VkPhysicalDevice device)
		{
			try (MemoryStack stack = stackPush())
			{
				IntBuffer extensionCount = stack.ints(0);
				vkEnumerateDeviceExtensionProperties(device, (String) null, extensionCount, null);
				VkExtensionProperties.Buffer availableExtensions = VkExtensionProperties.mallocStack(extensionCount.get(0), stack);
				return availableExtensions.stream().collect(toSet()).containsAll(DEVICE_EXTENSIONS);
			}
		}

		public static SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device, MemoryStack stack)
		{
			SwapChainSupportDetails details = new SwapChainSupportDetails();
			details.capabilities = VkSurfaceCapabilitiesKHR.mallocStack(stack);
			vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, details.capabilities);
			IntBuffer count = stack.ints(0);
			vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, count, null);
			if (count.get(0) != 0)
			{
				details.formats = VkSurfaceFormatKHR.mallocStack(count.get(0), stack);
				vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, count, details.formats);
			}
			vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, count, null);
			if (count.get(0) != 0)
			{
				details.presentModes = stack.mallocInt(count.get(0));
				vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, count, details.presentModes);
			}
			return details;
		}

		public static QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device)
		{
			QueueFamilyIndices indices = new QueueFamilyIndices();
			try (MemoryStack stack = stackPush())
			{
				IntBuffer queueFamilyCount = stack.ints(0);
				vkGetPhysicalDeviceQueueFamilyProperties(device, queueFamilyCount, null);
				VkQueueFamilyProperties.Buffer queueFamilies = VkQueueFamilyProperties.mallocStack(queueFamilyCount.get(0), stack);
				vkGetPhysicalDeviceQueueFamilyProperties(device, queueFamilyCount, queueFamilies);
				IntBuffer presentSupport = stack.ints(VK_FALSE);
				for (int i = 0; i < queueFamilies.capacity() || !indices.isComplete(); i++)
				{
					if ((queueFamilies.get(i).queueFlags() & VK_QUEUE_GRAPHICS_BIT) != 0)
					{ indices.graphicsFamily = i; }
					vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, presentSupport);
					if (presentSupport.get(0) == VK_TRUE)
					{ indices.presentFamily = i; }
				}
				return indices;
			}
		}

		private PointerBuffer asPointerBuffer(Collection<String> collection)
		{
			MemoryStack stack = stackGet();
			PointerBuffer buffer = stack.mallocPointer(collection.size());
			collection.stream()
				.map(stack::UTF8)
				.forEach(buffer::put);
			return buffer.rewind();
		}

		public static PointerBuffer asPointerBuffer(List<? extends Pointer> list)
		{
			MemoryStack stack = stackGet();
			PointerBuffer buffer = stack.mallocPointer(list.size());
			list.forEach(buffer::put);
			return buffer.rewind();
		}

		private PointerBuffer getRequiredExtensions()
		{
			PointerBuffer glfwExtensions = glfwGetRequiredInstanceExtensions();
			return glfwExtensions;
		}
	}

	public static void main(String[] args)
	{
		VulkanDemoGinger2 app = new VulkanDemoGinger2();
		app.run();
	}
}
