diff src/engine.nim @ 463:91544fc1afe5

did: hello world triangle, a bit of code organization
author Sam <sam@basx.dev>
date Mon, 19 Dec 2022 10:41:20 +0700
parents 0bc8643cfe25
children af9183acb173
line wrap: on
line diff
--- a/src/engine.nim	Fri Dec 16 00:05:41 2022 +0700
+++ b/src/engine.nim	Mon Dec 19 10:41:20 2022 +0700
@@ -1,4 +1,7 @@
+import std/strformat
 import std/enumerate
+import std/logging
+
 
 import ./vulkan
 import ./vulkan_helpers
@@ -6,6 +9,12 @@
 
 import ./glslang/glslang
 
+const MAX_FRAMES_IN_FLIGHT = 2
+
+var logger = newConsoleLogger()
+addHandler(logger)
+
+
 var vertexShaderCode: string = """#version 450
 layout(location = 0) out vec3 fragColor;
 vec3 colors[3] = vec3[](
@@ -39,10 +48,13 @@
 type
   GraphicsPipeline = object
     shaderStages*: seq[VkPipelineShaderStageCreateInfo]
+    layout*: VkPipelineLayout
+    renderPass*: VkRenderPass
+    pipeline*: VkPipeline
   QueueFamily = object
     properties*: VkQueueFamilyProperties
     hasSurfaceSupport*: bool
-  PhyscialDevice = object
+  PhysicalDevice = object
     device*: VkPhysicalDevice
     extensions*: seq[string]
     properties*: VkPhysicalDeviceProperties
@@ -52,61 +64,74 @@
     surfaceFormats: seq[VkSurfaceFormatKHR]
     presentModes: seq[VkPresentModeKHR]
   Vulkan* = object
+    debugMessenger: VkDebugUtilsMessengerEXT
     instance*: VkInstance
-    deviceList*: seq[PhyscialDevice]
-    activePhysicalDevice*: PhyscialDevice
-    activeQueueFamily*: uint32
+    deviceList*: seq[PhysicalDevice]
+    activePhysicalDevice*: PhysicalDevice
+    graphicsQueueFamily*: uint32
+    graphicsQueue*: VkQueue
+    presentationQueueFamily*: uint32
+    presentationQueue*: VkQueue
     device*: VkDevice
-    presentationQueue*: VkQueue
     surface*: VkSurfaceKHR
     selectedSurfaceFormat: VkSurfaceFormatKHR
     selectedPresentationMode: VkPresentModeKHR
-    selectedExtent: VkExtent2D
+    frameDimension: VkExtent2D
     swapChain: VkSwapchainKHR
     swapImages: seq[VkImage]
+    swapFramebuffers: seq[VkFramebuffer]
     swapImageViews: seq[VkImageView]
+    pipeline*: GraphicsPipeline
+    commandPool*: VkCommandPool
+    commandBuffers*: array[MAX_FRAMES_IN_FLIGHT, VkCommandBuffer]
+    viewport*: VkViewport
+    scissor*: VkRect2D
+    imageAvailableSemaphores*: array[MAX_FRAMES_IN_FLIGHT, VkSemaphore]
+    renderFinishedSemaphores*: array[MAX_FRAMES_IN_FLIGHT, VkSemaphore]
+    inFlightFences*: array[MAX_FRAMES_IN_FLIGHT, VkFence]
   Engine* = object
     display*: PDisplay
     window*: x.Window
     vulkan*: Vulkan
-    pipeline*: GraphicsPipeline
 
 
-proc getAllPhysicalDevices(instance: VkInstance, surface: VkSurfaceKHR): seq[PhyscialDevice] =
+proc getAllPhysicalDevices(instance: VkInstance, surface: VkSurfaceKHR): seq[PhysicalDevice] =
   for vulkanPhysicalDevice in getVulkanPhysicalDevices(instance):
-    var device = PhyscialDevice(device: vulkanPhysicalDevice, extensions: getDeviceExtensions(vulkanPhysicalDevice))
+    var device = PhysicalDevice(device: vulkanPhysicalDevice, extensions: getDeviceExtensions(vulkanPhysicalDevice))
     vkGetPhysicalDeviceProperties(vulkanPhysicalDevice, addr(device.properties))
     vkGetPhysicalDeviceFeatures(vulkanPhysicalDevice, addr(device.features))
     checkVkResult vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vulkanPhysicalDevice, surface, addr(device.surfaceCapabilities))
     device.surfaceFormats = getDeviceSurfaceFormats(vulkanPhysicalDevice, surface)
     device.presentModes = getDeviceSurfacePresentModes(vulkanPhysicalDevice, surface)
 
+    debug(&"Physical device nr {int(vulkanPhysicalDevice)} {cleanString(device.properties.deviceName)}")
     for i, queueFamilyProperty in enumerate(getQueueFamilies(vulkanPhysicalDevice)):
       var hasSurfaceSupport: VkBool32 = VK_FALSE
       checkVkResult vkGetPhysicalDeviceSurfaceSupportKHR(vulkanPhysicalDevice, uint32(i), surface, addr(hasSurfaceSupport))
       device.queueFamilies.add(QueueFamily(properties: queueFamilyProperty, hasSurfaceSupport: bool(hasSurfaceSupport)))
+      debug(&"  Queue family {i} {queueFamilyProperty}")
 
     result.add(device)
 
-proc filterForDevice(devices: seq[PhyscialDevice]): seq[(PhyscialDevice, uint32)] =
+proc filterForDevice(devices: seq[PhysicalDevice]): seq[(PhysicalDevice, uint32, uint32)] =
   for device in devices:
-    if "VK_KHR_swapchain" in device.extensions:
-      for i, queueFamily in enumerate(device.queueFamilies):
-        let hasGraphics = bool(uint32(queueFamily.properties.queueFlags) and ord(VK_QUEUE_GRAPHICS_BIT))
-        if (
-          queueFamily.hasSurfaceSupport and
-          hasGraphics and
-          device.surfaceFormats.len > 0 and
-          device.presentModes.len > 0
-        ):
-          result.add((device, uint32(i)))
+    if not (device.surfaceFormats.len > 0 and device.presentModes.len > 0 and "VK_KHR_swapchain" in device.extensions):
+      continue
+    var graphicsQueueFamily = high(uint32)
+    var presentationQueueFamily = high(uint32)
+    for i, queueFamily in enumerate(device.queueFamilies):
+      if queueFamily.hasSurfaceSupport:
+        presentationQueueFamily = uint32(i)
+      if bool(uint32(queueFamily.properties.queueFlags) and ord(VK_QUEUE_GRAPHICS_BIT)):
+        graphicsQueueFamily = uint32(i)
+    if graphicsQueueFamily != high(uint32) and presentationQueueFamily != high(uint32):
+      result.add((device, graphicsQueueFamily, presentationQueueFamily))
 
-proc filterForSurfaceFormat(formats: seq[VkSurfaceFormatKHR]): seq[VkSurfaceFormatKHR] =
-  for format in formats:
-    if format.format == VK_FORMAT_B8G8R8A8_SRGB and format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR:
-      result.add(format)
+  for (device, graphicsQueueFamily, presentationQueueFamily) in result:
+    debug(&"Viable device: {cleanString(device.properties.deviceName)} (graphics queue family {graphicsQueueFamily}, presentation queue family {presentationQueueFamily})")
 
-proc getSwapExtent(display: PDisplay, window: Window, capabilities: VkSurfaceCapabilitiesKHR): VkExtent2D =
+
+proc getFrameDimension(display: PDisplay, window: Window, capabilities: VkSurfaceCapabilitiesKHR): VkExtent2D =
   if capabilities.currentExtent.width != high(uint32):
     return capabilities.currentExtent
   else:
@@ -116,37 +141,69 @@
       height: min(max(uint32(height), capabilities.minImageExtent.height), capabilities.maxImageExtent.height),
     )
 
+proc createVulkanSurface(instance: VkInstance, display: PDisplay, window: Window): VkSurfaceKHR =
+  var surfaceCreateInfo = VkXlibSurfaceCreateInfoKHR(
+    sType: VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR,
+    dpy: display,
+    window: window,
+  )
+  checkVkResult vkCreateXlibSurfaceKHR(instance, addr(surfaceCreateInfo), nil, addr(result))
+
+proc setupVulkanDeviceAndQueues(instance: VkInstance, surface: VkSurfaceKHR): (PhysicalDevice, uint32, uint32, VkDevice, VkQueue, VkQueue) =
+  let usableDevices = instance.getAllPhysicalDevices(surface).filterForDevice()
+  if len(usableDevices) == 0:
+    raise newException(Exception, "No suitable graphics device found")
+  result[0] = usableDevices[0][0]
+  result[1] = usableDevices[0][1]
+  result[2] = usableDevices[0][2]
+
+  debug(&"Chose device {cleanString(result[0].properties.deviceName)}")
+  
+  (result[3], result[4], result[5]) = getVulcanDevice(
+    result[0].device,
+    result[0].features,
+    result[1],
+    result[2],
+  )
+
 proc igniteEngine*(): Engine =
-  vkLoad1_0()
-  vkLoad1_1()
-  vkLoad1_2()
 
   # init X11 window
   (result.display, result.window) = xlibInit()
 
   # create vulkan instance
+  vkLoad1_0()
+  vkLoad1_1()
+  vkLoad1_2()
   result.vulkan.instance = createVulkanInstance(VULKAN_VERSION)
-
-  # create vulkan-X11 surface
-  var surfaceCreateInfo = VkXlibSurfaceCreateInfoKHR(
-    sType: VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR,
-    dpy: result.display,
-    window: result.window,
-  )
-  checkVkResult vkCreateXlibSurfaceKHR(result.vulkan.instance, addr(surfaceCreateInfo), nil, addr(result.vulkan.surface))
+  when ENABLEVULKANVALIDATIONLAYERS:
+    var createInfo = VkDebugUtilsMessengerCreateInfoEXT(
+      sType: VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
+      messageSeverity: VkDebugUtilsMessageSeverityFlagsEXT(
+        ord(VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) or
+        ord(VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) or
+        ord(VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT)
+      ),
+      messageType: VkDebugUtilsMessageTypeFlagsEXT(
+        ord(VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT) or
+        ord(VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) or
+        ord(VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT)
+      ),
+      pfnUserCallback: debugCallback,
+      pUserData: nil,
+    )
+    checkVkResult vkCreateDebugUtilsMessengerEXT(result.vulkan.instance, addr(createInfo), nil, addr(result.vulkan.debugMessenger))
 
-  # determine device and queue to use and instantiate
-  result.vulkan.deviceList = result.vulkan.instance.getAllPhysicalDevices(result.vulkan.surface)
-  let usableDevices = result.vulkan.deviceList.filterForDevice()
-  if len(usableDevices) == 0:
-    raise newException(Exception, "No suitable graphics device found")
-  (result.vulkan.activePhysicalDevice, result.vulkan.activeQueueFamily) = usableDevices[0]
-  
-  (result.vulkan.device, result.vulkan.presentationQueue) = getVulcanDevice(
-    result.vulkan.activePhysicalDevice.device,
-    result.vulkan.activePhysicalDevice.features,
-    result.vulkan.activeQueueFamily
-  )
+  result.vulkan.surface = result.vulkan.instance.createVulkanSurface(result.display, result.window)
+
+  (
+    result.vulkan.activePhysicalDevice,
+    result.vulkan.graphicsQueueFamily,
+    result.vulkan.presentationQueueFamily,
+    result.vulkan.device,
+    result.vulkan.graphicsQueue,
+    result.vulkan.presentationQueue
+  ) = result.vulkan.instance.setupVulkanDeviceAndQueues(result.vulkan.surface)
   
   # determine surface format for swapchain
   let usableSurfaceFormats = filterForSurfaceFormat(result.vulkan.activePhysicalDevice.surfaceFormats)
@@ -154,7 +211,7 @@
     raise newException(Exception, "No suitable surface formats found")
   result.vulkan.selectedSurfaceFormat = usableSurfaceFormats[0]
   result.vulkan.selectedPresentationMode = getPresentMode(result.vulkan.activePhysicalDevice.presentModes)
-  result.vulkan.selectedExtent = getSwapExtent(result.display, result.window, result.vulkan.activePhysicalDevice.surfaceCapabilities)
+  result.vulkan.frameDimension = result.display.getFrameDimension(result.window, result.vulkan.activePhysicalDevice.surfaceCapabilities)
 
   # setup swapchain
   var swapchainCreateInfo = VkSwapchainCreateInfoKHR(
@@ -163,7 +220,7 @@
     minImageCount: max(result.vulkan.activePhysicalDevice.surfaceCapabilities.minImageCount + 1, result.vulkan.activePhysicalDevice.surfaceCapabilities.maxImageCount),
     imageFormat: result.vulkan.selectedSurfaceFormat.format,
     imageColorSpace: result.vulkan.selectedSurfaceFormat.colorSpace,
-    imageExtent: result.vulkan.selectedExtent,
+    imageExtent: result.vulkan.frameDimension,
     imageArrayLayers: 1,
     imageUsage: VkImageUsageFlags(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT),
     # VK_SHARING_MODE_CONCURRENT no supported (i.e cannot use different queue families for  drawing to swap surface?)
@@ -174,8 +231,8 @@
     clipped: VK_TRUE,
     oldSwapchain: VkSwapchainKHR(0),
   )
-  checkVkResult vkCreateSwapchainKHR(result.vulkan.device, addr(swapchainCreateInfo), nil, addr(result.vulkan.swapChain))
-  result.vulkan.swapImages = getSwapChainImages(result.vulkan.device, result.vulkan.swapChain)
+  checkVkResult result.vulkan.device.vkCreateSwapchainKHR(addr(swapchainCreateInfo), nil, addr(result.vulkan.swapChain))
+  result.vulkan.swapImages = result.vulkan.device.getSwapChainImages(result.vulkan.swapChain)
 
   # setup swapchian image views
   result.vulkan.swapImageViews = newSeq[VkImageView](result.vulkan.swapImages.len)
@@ -199,77 +256,344 @@
         layerCount: 1,
       ),
     )
-    checkVkResult vkCreateImageView(result.vulkan.device, addr(imageViewCreateInfo), nil, addr(result.vulkan.swapImageViews[i]))
+    checkVkResult result.vulkan.device.vkCreateImageView(addr(imageViewCreateInfo), nil, addr(result.vulkan.swapImageViews[i]))
 
   # init shader system
   checkGlslangResult glslang_initialize_process()
 
   # load shaders
-  result.pipeline.shaderStages.add(createShaderStage(result.vulkan.device, VK_SHADER_STAGE_VERTEX_BIT, vertexShaderCode))
-  result.pipeline.shaderStages.add(createShaderStage(result.vulkan.device, VK_SHADER_STAGE_FRAGMENT_BIT, fragmentShaderCode))
+  result.vulkan.pipeline.shaderStages.add(result.vulkan.device.createShaderStage(VK_SHADER_STAGE_VERTEX_BIT, vertexShaderCode))
+  result.vulkan.pipeline.shaderStages.add(result.vulkan.device.createShaderStage(VK_SHADER_STAGE_FRAGMENT_BIT, fragmentShaderCode))
+
+  # setup render passes
+  var
+    colorAttachment = VkAttachmentDescription(
+      format: result.vulkan.selectedSurfaceFormat.format,
+      samples: VK_SAMPLE_COUNT_1_BIT,
+      loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR,
+      storeOp: VK_ATTACHMENT_STORE_OP_STORE,
+      stencilLoadOp: VK_ATTACHMENT_LOAD_OP_DONT_CARE,
+      stencilStoreOp: VK_ATTACHMENT_STORE_OP_DONT_CARE,
+      initialLayout: VK_IMAGE_LAYOUT_UNDEFINED,
+      finalLayout: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
+    )
+    colorAttachmentRef = VkAttachmentReference(
+      attachment: 0,
+      layout: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+    )
+    subpass = VkSubpassDescription(
+      pipelineBindPoint: VK_PIPELINE_BIND_POINT_GRAPHICS,
+      colorAttachmentCount: 1,
+      pColorAttachments: addr(colorAttachmentRef)
+    )
+    dependency = VkSubpassDependency(
+      srcSubpass: VK_SUBPASS_EXTERNAL,
+      dstSubpass: 0,
+      srcStageMask: VkPipelineStageFlags(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT),
+      srcAccessMask: VkAccessFlags(0),
+      dstStageMask: VkPipelineStageFlags(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT),
+      dstAccessMask: VkAccessFlags(VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT),
+    )
+    renderPassCreateInfo = VkRenderPassCreateInfo(
+      sType: VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
+      attachmentCount: 1,
+      pAttachments: addr(colorAttachment),
+      subpassCount: 1,
+      pSubpasses: addr(subpass),
+      dependencyCount: 1,
+      pDependencies: addr(dependency),
+    )
+  checkVkResult result.vulkan.device.vkCreateRenderPass(addr(renderPassCreateInfo), nil, addr(result.vulkan.pipeline.renderPass))
 
   # create graphis pipeline
-  var dynamicStates = [VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR]
-  var dynamicState = VkPipelineDynamicStateCreateInfo(
-    sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
-    dynamicStateCount: uint32(dynamicStates.len),
-    pDynamicStates: addr(dynamicStates[0]),
-  )
-  var vertexInputInfo = VkPipelineVertexInputStateCreateInfo(
-    sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
-    vertexBindingDescriptionCount: 0,
-    pVertexBindingDescriptions: nil,
-    vertexAttributeDescriptionCount: 0,
-    pVertexAttributeDescriptions: nil,
-  )
-  var inputAssembly = VkPipelineInputAssemblyStateCreateInfo(
-    sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
-    topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
-    primitiveRestartEnable: VK_FALSE,
-  )
+  
+  var
+    # define which parts can be dynamic (pipeline is fixed after setup)
+    dynamicStates = [VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR]
+    dynamicState = VkPipelineDynamicStateCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
+      dynamicStateCount: uint32(dynamicStates.len),
+      pDynamicStates: addr(dynamicStates[0]),
+    )
+
+    # define input data format
+    vertexInputInfo = VkPipelineVertexInputStateCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
+      vertexBindingDescriptionCount: 0,
+      pVertexBindingDescriptions: nil,
+      vertexAttributeDescriptionCount: 0,
+      pVertexAttributeDescriptions: nil,
+    )
+    inputAssembly = VkPipelineInputAssemblyStateCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
+      topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
+      primitiveRestartEnable: VK_FALSE,
+    )
 
   # setup viewport
-  var viewport = VkViewport(
+  result.vulkan.viewport = VkViewport(
     x: 0.0,
     y: 0.0,
-    width: (float) result.vulkan.selectedExtent.width,
-    height: (float) result.vulkan.selectedExtent.height,
+    width: (float) result.vulkan.frameDimension.width,
+    height: (float) result.vulkan.frameDimension.height,
     minDepth: 0.0,
     maxDepth: 1.0,
   )
-  var scissor = VkRect2D(
+  result.vulkan.scissor = VkRect2D(
     offset: VkOffset2D(x: 0, y: 0),
-    extent: result.vulkan.selectedExtent
+    extent: result.vulkan.frameDimension
+  )
+  var viewportState = VkPipelineViewportStateCreateInfo(
+    sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
+    viewportCount: 1,
+    pViewports: addr(result.vulkan.viewport),
+    scissorCount: 1,
+    pScissors: addr(result.vulkan.scissor),
   )
 
+  # rasterizerization config
+  var
+    rasterizer = VkPipelineRasterizationStateCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
+      depthClampEnable: VK_FALSE,
+      rasterizerDiscardEnable: VK_FALSE,
+      polygonMode: VK_POLYGON_MODE_FILL,
+      lineWidth: 1.0,
+      cullMode: VkCullModeFlags(VK_CULL_MODE_BACK_BIT),
+      frontFace: VK_FRONT_FACE_CLOCKWISE,
+      depthBiasEnable: VK_FALSE,
+      depthBiasConstantFactor: 0.0,
+      depthBiasClamp: 0.0,
+      depthBiasSlopeFactor: 0.0,
+    )
+    multisampling = VkPipelineMultisampleStateCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
+      sampleShadingEnable: VK_FALSE,
+      rasterizationSamples: VK_SAMPLE_COUNT_1_BIT,
+      minSampleShading: 1.0,
+      pSampleMask: nil,
+      alphaToCoverageEnable: VK_FALSE,
+      alphaToOneEnable: VK_FALSE,
+    )
+    colorBlendAttachment = VkPipelineColorBlendAttachmentState(
+      colorWriteMask: VkColorComponentFlags(
+        ord(VK_COLOR_COMPONENT_R_BIT) or
+        ord(VK_COLOR_COMPONENT_G_BIT) or
+        ord(VK_COLOR_COMPONENT_B_BIT) or
+        ord(VK_COLOR_COMPONENT_A_BIT)
+      ),
+      blendEnable: VK_TRUE,
+      srcColorBlendFactor: VK_BLEND_FACTOR_SRC_ALPHA,
+      dstColorBlendFactor: VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
+      colorBlendOp: VK_BLEND_OP_ADD,
+      srcAlphaBlendFactor: VK_BLEND_FACTOR_ONE,
+      dstAlphaBlendFactor: VK_BLEND_FACTOR_ZERO,
+      alphaBlendOp: VK_BLEND_OP_ADD,
+    )
+    colorBlending = VkPipelineColorBlendStateCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
+      logicOpEnable: VK_TRUE,
+      logicOp: VK_LOGIC_OP_COPY,
+      attachmentCount: 1,
+      pAttachments: addr(colorBlendAttachment),
+      blendConstants: [0.0'f, 0.0'f, 0.0'f, 0.0'f],
+    )
+
+    # create pipeline
+    pipelineLayoutInfo = VkPipelineLayoutCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
+      setLayoutCount: 0,
+      pSetLayouts: nil,
+      pushConstantRangeCount: 0,
+      pPushConstantRanges: nil,
+    )
+  checkVkResult result.vulkan.device.vkCreatePipelineLayout(addr(pipelineLayoutInfo), nil, addr(result.vulkan.pipeline.layout))
+
+  var pipelineInfo = VkGraphicsPipelineCreateInfo(
+    sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
+    stageCount: 2,
+    pStages: addr(result.vulkan.pipeline.shaderStages[0]),
+    pVertexInputState: addr(vertexInputInfo),
+    pInputAssemblyState: addr(inputAssembly),
+    pViewportState: addr(viewportState),
+    pRasterizationState: addr(rasterizer),
+    pMultisampleState: addr(multisampling),
+    pDepthStencilState: nil,
+    pColorBlendState: addr(colorBlending),
+    pDynamicState: addr(dynamicState),
+    layout: result.vulkan.pipeline.layout,
+    renderPass: result.vulkan.pipeline.renderPass,
+    subpass: 0,
+    basePipelineHandle: VkPipeline(0),
+    basePipelineIndex: -1,
+  )
+  checkVkResult result.vulkan.device.vkCreateGraphicsPipelines(
+    VkPipelineCache(0),
+    1,
+    addr(pipelineInfo),
+    nil,
+    addr(result.vulkan.pipeline.pipeline)
+  )
+
+  # set up framebuffers
+  result.vulkan.swapFramebuffers  = newSeq[VkFramebuffer](result.vulkan.swapImages.len)
+
+  for i, imageview in enumerate(result.vulkan.swapImageViews):
+    var framebufferInfo = VkFramebufferCreateInfo(
+      sType: VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
+      renderPass: result.vulkan.pipeline.renderPass,
+      attachmentCount: 1,
+      pAttachments: addr(result.vulkan.swapImageViews[i]),
+      width: result.vulkan.frameDimension.width,
+      height: result.vulkan.frameDimension.height,
+      layers: 1,
+    )
+    checkVkResult result.vulkan.device.vkCreateFramebuffer(addr(framebufferInfo), nil, addr(result.vulkan.swapFramebuffers[i]))
+  
+  # set up command buffer
+  var poolInfo = VkCommandPoolCreateInfo(
+    sType: VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+    flags: VkCommandPoolCreateFlags(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT),
+    queueFamilyIndex: result.vulkan.graphicsQueueFamily,
+  )
+  checkVkResult result.vulkan.device.vkCreateCommandPool(addr(poolInfo), nil, addr(result.vulkan.commandPool))
+
+  var allocInfo = VkCommandBufferAllocateInfo(
+    sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+    commandPool: result.vulkan.commandPool,
+    level: VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+    commandBufferCount: result.vulkan.commandBuffers.len.uint32,
+  )
+  checkVkResult result.vulkan.device.vkAllocateCommandBuffers(addr(allocInfo), addr(result.vulkan.commandBuffers[0]))
+
+  # create semaphores for syncing rendering
+  var semaphoreInfo = VkSemaphoreCreateInfo(sType: VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO)
+  var fenceInfo = VkFenceCreateInfo(
+    sType: VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+    flags: VkFenceCreateFlags(VK_FENCE_CREATE_SIGNALED_BIT)
+  )
+  for i in 0 ..< MAX_FRAMES_IN_FLIGHT:
+    checkVkResult result.vulkan.device.vkCreateSemaphore(addr(semaphoreInfo), nil, addr(result.vulkan.imageAvailableSemaphores[i]))
+    checkVkResult result.vulkan.device.vkCreateSemaphore(addr(semaphoreInfo), nil, addr(result.vulkan.renderFinishedSemaphores[i]))
+    checkVkResult result.vulkan.device.vkCreateFence(addr(fenceInfo), nil, addr(result.vulkan.inFlightFences[i]))
+
 
-proc fullThrottle*(engine: Engine) =
-  var event: XEvent
-  while true:
-    discard XNextEvent(engine.display, addr(event))
-    case event.theType
-    of Expose:
-      discard
-    of ClientMessage:
-      if cast[Atom](event.xclient.data.l[0]) == deleteMessage:
-        break
-    of KeyPress:
-      let key = XLookupKeysym(cast[PXKeyEvent](addr(event)), 0)
-      if key != 0:
-        echo "Key ", key, " pressed"
-    of ButtonPressMask:
-      echo "Mouse button ", event.xbutton.button, " pressed at ",
-          event.xbutton.x, ",", event.xbutton.y
-    else:
-      discard
+proc recordCommandBuffer(vulkan: var Vulkan, commandBuffer: VkCommandBuffer, imageIndex: uint32) =
+  var beginInfo = VkCommandBufferBeginInfo(
+    sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+    pInheritanceInfo: nil,
+  )
+  checkVkResult commandBuffer.vkBeginCommandBuffer(addr(beginInfo))
+
+  var
+    clearColor = VkClearValue(color: VkClearColorValue(float32: [0.2'f, 0.2'f, 0.2'f, 1.0'f]))
+    renderPassInfo = VkRenderPassBeginInfo(
+      sType: VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
+      renderPass: vulkan.pipeline.renderPass,
+      framebuffer: vulkan.swapFramebuffers[imageIndex],
+      renderArea: VkRect2D(
+        offset: VkOffset2D(x: 0, y: 0),
+        extent: vulkan.frameDimension,
+      ),
+      clearValueCount: 1,
+      pClearValues: addr(clearColor),
+    )
+  commandBuffer.vkCmdBeginRenderPass(addr(renderPassInfo), VK_SUBPASS_CONTENTS_INLINE)
+  commandBuffer.vkCmdBindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, vulkan.pipeline.pipeline)
+
+  commandBuffer.vkCmdSetViewport(firstViewport=0, viewportCount=1, addr(vulkan.viewport))
+  commandBuffer.vkCmdSetScissor(firstScissor=0, scissorCount=1, addr(vulkan.scissor))
+  commandBuffer.vkCmdDraw(vertexCount=3, instanceCount=1, firstVertex=0, firstInstance=0)
+  commandBuffer.vkCmdEndRenderPass()
+  checkVkResult commandBuffer.vkEndCommandBuffer()
+
+proc drawFrame(vulkan: var Vulkan, currentFrame: int) =
+  checkVkResult vulkan.device.vkWaitForFences(1, addr(vulkan.inFlightFences[currentFrame]), VK_TRUE, high(uint64))
+  checkVkResult vulkan.device.vkResetFences(1, addr(vulkan.inFlightFences[currentFrame]))
+  var bufferImageIndex: uint32
+  checkVkResult vulkan.device.vkAcquireNextImageKHR(
+    vulkan.swapChain,
+    high(uint64),
+    vulkan.imageAvailableSemaphores[currentFrame],
+    VkFence(0),
+    addr(bufferImageIndex)
+  )
+
+  checkVkResult vkResetCommandBuffer(vulkan.commandBuffers[currentFrame], VkCommandBufferResetFlags(0))
+  recordCommandBuffer(vulkan, vulkan.commandBuffers[currentFrame], bufferImageIndex)
+  var
+    waitSemaphores = [vulkan.imageAvailableSemaphores[currentFrame]]
+    waitStages = [VkPipelineStageFlags(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT)]
+    signalSemaphores = [vulkan.renderFinishedSemaphores[currentFrame]]
+    submitInfo = VkSubmitInfo(
+      sType: VK_STRUCTURE_TYPE_SUBMIT_INFO,
+      waitSemaphoreCount: 1,
+      pWaitSemaphores: addr(waitSemaphores[0]),
+      pWaitDstStageMask: addr(waitStages[0]),
+      commandBufferCount: 1,
+      pCommandBuffers: addr(vulkan.commandBuffers[currentFrame]),
+      signalSemaphoreCount: 1,
+      pSignalSemaphores: addr(signalSemaphores[0]),
+    )
+  checkVkResult vkQueueSubmit(vulkan.graphicsQueue, 1, addr(submitInfo), vulkan.inFlightFences[currentFrame])
+
+  var presentInfo = VkPresentInfoKHR(
+    sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
+    waitSemaphoreCount: 1,
+    pWaitSemaphores: addr(signalSemaphores[0]),
+    swapchainCount: 1,
+    pSwapchains: addr(vulkan.swapChain),
+    pImageIndices: addr(bufferImageIndex),
+    pResults: nil,
+  )
+  checkVkResult vkQueuePresentKHR(vulkan.presentationQueue, addr(presentInfo))
+
+
+proc fullThrottle*(engine: var Engine) =
+  var
+    event: XEvent
+    killed = false
+    currentFrame = 0
+
+  while not killed:
+    while engine.display.XPending() > 0 and not killed:
+      discard engine.display.XNextEvent(addr(event))
+      case event.theType
+      of ClientMessage:
+        if cast[Atom](event.xclient.data.l[0]) == deleteMessage:
+          killed = true
+      of KeyPress:
+        let key = XLookupKeysym(cast[PXKeyEvent](addr(event)), 0)
+        if key == XK_Escape:
+          killed = true
+      else:
+        discard
+    drawFrame(engine.vulkan, currentFrame)
+    currentFrame = (currentFrame + 1) mod MAX_FRAMES_IN_FLIGHT;
+  checkVkResult engine.vulkan.device.vkDeviceWaitIdle()
+
 
 proc trash*(engine: Engine) =
-  for shaderStage in engine.pipeline.shaderStages:
-    vkDestroyShaderModule(engine.vulkan.device, shaderStage.module, nil);
+  for i in 0 ..< MAX_FRAMES_IN_FLIGHT:
+    engine.vulkan.device.vkDestroySemaphore(engine.vulkan.imageAvailableSemaphores[i], nil)
+    engine.vulkan.device.vkDestroySemaphore(engine.vulkan.renderFinishedSemaphores[i], nil)
+    engine.vulkan.device.vkDestroyFence(engine.vulkan.inFlightFences[i], nil)
+
+  engine.vulkan.device.vkDestroyCommandPool(engine.vulkan.commandPool, nil)
+  for framebuffer in engine.vulkan.swapFramebuffers:
+    engine.vulkan.device.vkDestroyFramebuffer(framebuffer, nil)
+
+  engine.vulkan.device.vkDestroyPipeline(engine.vulkan.pipeline.pipeline, nil)
+  engine.vulkan.device.vkDestroyPipelineLayout(engine.vulkan.pipeline.layout, nil)
+  engine.vulkan.device.vkDestroyRenderPass(engine.vulkan.pipeline.renderPass, nil)
+
+  for shaderStage in engine.vulkan.pipeline.shaderStages:
+    engine.vulkan.device.vkDestroyShaderModule(shaderStage.module, nil)
+
   glslang_finalize_process()
-  vkDestroySwapchainKHR(engine.vulkan.device, engine.vulkan.swapChain, nil);
-  vkDestroySurfaceKHR(engine.vulkan.instance, engine.vulkan.surface, nil);
-  vkDestroyDevice(engine.vulkan.device, nil)
-  vkDestroyInstance(engine.vulkan.instance, nil)
+  engine.vulkan.device.vkDestroySwapchainKHR(engine.vulkan.swapChain, nil)
+  engine.vulkan.instance.vkDestroySurfaceKHR(engine.vulkan.surface, nil)
+  engine.vulkan.device.vkDestroyDevice(nil)
+  engine.vulkan.instance.vkDestroyInstance(nil)
   checkXlibResult engine.display.XDestroyWindow(engine.window)
   discard engine.display.XCloseDisplay() # always returns 0