changeset 99:4deffc94484a

add: vertex and (initial) shader types and methods
author Sam <sam@basx.dev>
date Tue, 14 Mar 2023 13:21:40 +0700
parents 335a65d3f892
children f4c6ff03b12c
files src/semicongine/engine.nim src/semicongine/shader.nim src/semicongine/vertex.nim src/semicongine/vulkan.nim src/semicongine/vulkan/pipeline.nim src/semicongine/vulkan/renderpass.nim src/semicongine/vulkan/shader.nim src/semicongine/vulkan/syncing.nim src/semicongine/vulkan/vertex.nim tests/test_vulkan_wrapper.nim
diffstat 10 files changed, 431 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine/engine.nim	Mon Mar 06 23:50:21 2023 +0700
+++ b/src/semicongine/engine.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -294,15 +294,11 @@
     )
   checkVkResult device.vkCreateRenderPass(addr(renderPassCreateInfo), nil, addr(result))
 
-proc initRenderPipeline[VertexType, Uniforms](device: VkDevice,
-    frameSize: TVec2[uint32], renderPass: VkRenderPass, vertexShader,
-    fragmentShader: static string): RenderPipeline[VertexType, Uniforms] =
+proc initRenderPipeline[VertexType, Uniforms](device: VkDevice, frameSize: TVec2[uint32], renderPass: VkRenderPass, vertexShader, fragmentShader: static string): RenderPipeline[VertexType, Uniforms] =
   # load shaders
   result.device = device
-  result.shaders.add(initShaderProgram[VertexType, Uniforms](device,
-      VK_SHADER_STAGE_VERTEX_BIT, vertexShader))
-  result.shaders.add(initShaderProgram[VertexType, Uniforms](device,
-      VK_SHADER_STAGE_FRAGMENT_BIT, fragmentShader))
+  result.shaders.add(initShaderProgram[VertexType, Uniforms](device, VK_SHADER_STAGE_VERTEX_BIT, vertexShader))
+  result.shaders.add(initShaderProgram[VertexType, Uniforms](device, VK_SHADER_STAGE_FRAGMENT_BIT, fragmentShader))
 
   var
     # define which parts can be dynamic (pipeline is fixed after setup)
@@ -395,8 +391,7 @@
       pushConstantRangeCount: 0,
       pPushConstantRanges: nil,
     )
-  checkVkResult vkCreatePipelineLayout(device, addr(pipelineLayoutInfo), nil,
-      addr(result.layout))
+  checkVkResult vkCreatePipelineLayout(device, addr(pipelineLayoutInfo), nil, addr(result.layout))
 
   var stages: seq[VkPipelineShaderStageCreateInfo]
   for shader in result.shaders:
@@ -559,9 +554,7 @@
   ) = result.vulkan.device.device.setupSyncPrimitives()
 
 
-proc setupPipeline*[VertexType; UniformType; IndexType: uint16|uint32](
-  engine: var Engine, scenedata: Thing, vertexShader,
-    fragmentShader: static string): RenderPipeline[VertexType, UniformType] =
+proc setupPipeline*[VertexType; UniformType; IndexType: uint16|uint32](engine: var Engine, scenedata: Thing, vertexShader, fragmentShader: static string): RenderPipeline[VertexType, UniformType] =
   engine.currentscenedata = scenedata
   result = initRenderPipeline[VertexType, UniformType](
     engine.vulkan.device.device,
@@ -670,8 +663,7 @@
   vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
       pipeline.layout, 0, 1, addr(pipeline.descriptors[currentFrame]), 0, nil)
 
-  for (vertexBufferSet, indexed, indexBuffer, count, indexType) in
-    pipeline.vertexBuffers:
+  for (vertexBufferSet, indexed, indexBuffer, count, indexType) in pipeline.vertexBuffers:
     var
       vertexBuffers: seq[VkBuffer]
       offsets: seq[VkDeviceSize]
@@ -679,9 +671,7 @@
       vertexBuffers.add buffer.vkBuffer
       offsets.add VkDeviceSize(0)
 
-    vkCmdBindVertexBuffers(commandBuffer, firstBinding = 0'u32,
-        bindingCount = uint32(vertexBuffers.len), pBuffers = addr(vertexBuffers[
-            0]), pOffsets = addr(offsets[0]))
+    vkCmdBindVertexBuffers(commandBuffer, firstBinding = 0'u32, bindingCount = uint32(vertexBuffers.len), pBuffers = addr(vertexBuffers[ 0]), pOffsets = addr(offsets[0]))
     if indexed:
       vkCmdBindIndexBuffer(commandBuffer, indexBuffer.vkBuffer, VkDeviceSize(0), indexType)
       vkCmdDrawIndexed(commandBuffer, count, 1, 0, 0, 0)
@@ -731,10 +721,8 @@
     vkCmdEndRenderPass(commandBuffer)
   checkVkResult vkEndCommandBuffer(commandBuffer)
 
-proc drawFrame(window: NativeWindow, vulkan: var Vulkan, currentFrame: int,
-    resized: bool, pipeline: var RenderPipeline) =
-  checkVkResult vkWaitForFences(vulkan.device.device, 1, addr(
-      vulkan.inFlightFences[currentFrame]), VK_TRUE, high(uint64))
+proc drawFrame(window: NativeWindow, vulkan: var Vulkan, currentFrame: int, resized: bool, pipeline: var RenderPipeline) =
+  checkVkResult vkWaitForFences(vulkan.device.device, 1, addr(vulkan.inFlightFences[currentFrame]), VK_TRUE, high(uint64))
   var bufferImageIndex: uint32
   let nextImageResult = vkAcquireNextImageKHR(
     vulkan.device.device,
@@ -751,8 +739,7 @@
   elif not (nextImageResult in [VK_SUCCESS, VK_SUBOPTIMAL_KHR]):
     raise newException(Exception, "Vulkan error: vkAcquireNextImageKHR returned " &
         $nextImageResult)
-  checkVkResult vkResetFences(vulkan.device.device, 1, addr(
-      vulkan.inFlightFences[currentFrame]))
+  checkVkResult vkResetFences(vulkan.device.device, 1, addr(vulkan.inFlightFences[currentFrame]))
 
   checkVkResult vkResetCommandBuffer(vulkan.device.commandBuffers[currentFrame],
       VkCommandBufferResetFlags(0))
@@ -772,8 +759,7 @@
       signalSemaphoreCount: 1,
       pSignalSemaphores: addr(signalSemaphores[0]),
     )
-  checkVkResult vkQueueSubmit(vulkan.device.graphicsQueue, 1, addr(submitInfo),
-      vulkan.inFlightFences[currentFrame])
+  checkVkResult vkQueueSubmit(vulkan.device.graphicsQueue, 1, addr(submitInfo), vulkan.inFlightFences[currentFrame])
 
   var presentInfo = VkPresentInfoKHR(
     sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
--- a/src/semicongine/shader.nim	Mon Mar 06 23:50:21 2023 +0700
+++ b/src/semicongine/shader.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -41,9 +41,7 @@
   of VK_SHADER_STAGE_COMPUTE_BIT: "comp"
   else: ""
 
-proc compileGLSLToSPIRV(stage: static VkShaderStageFlagBits,
-    shaderSource: static string, entrypoint: string): seq[
-    uint32] {.compileTime.} =
+proc compileGLSLToSPIRV(stage: static VkShaderStageFlagBits, shaderSource: static string, entrypoint: string): seq[uint32] {.compileTime.} =
   when defined(nimcheck): # will not run if nimcheck is running
     return result
   const
@@ -79,9 +77,7 @@
     )
     i += 4
 
-proc initShaderProgram*[VertexType, Uniforms](device: VkDevice,
-    programType: static VkShaderStageFlagBits, shader: static string,
-    entryPoint: static string = "main"): ShaderProgram[VertexType, Uniforms] =
+proc initShaderProgram*[VertexType, Uniforms](device: VkDevice, programType: static VkShaderStageFlagBits, shader: static string, entryPoint: static string = "main"): ShaderProgram[VertexType, Uniforms] =
   result.entryPoint = entryPoint
   result.programType = programType
 
--- a/src/semicongine/vertex.nim	Mon Mar 06 23:50:21 2023 +0700
+++ b/src/semicongine/vertex.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -142,8 +142,7 @@
   return stmtList.join("\n")
 
 
-func generateInputVertexBinding*[T](bindingoffset: int = 0,
-    locationoffset: int = 0): seq[VkVertexInputBindingDescription] =
+func generateInputVertexBinding*[T](bindingoffset: int = 0, locationoffset: int = 0): seq[VkVertexInputBindingDescription] =
   # packed attribute data, not interleaved (aks "struct of arrays")
   var binding = bindingoffset
   for name, value in T().fieldPairs:
@@ -163,8 +162,7 @@
     binding += 1
 
 
-func generateInputAttributeBinding*[T](bindingoffset: int = 0,
-    locationoffset: int = 0): seq[VkVertexInputAttributeDescription] =
+func generateInputAttributeBinding*[T](bindingoffset: int = 0, locationoffset: int = 0): seq[VkVertexInputAttributeDescription] =
   # TODO:
   var location = 0
   var binding = bindingoffset
@@ -175,12 +173,9 @@
           VkVertexInputAttributeDescription(
             binding: uint32(binding),
             location: uint32(location),
-            format: getVkFormat[compositeAttributeType(getAttributeType(
-                value))](),
-            offset: uint32(i * sizeof(compositeAttributeType(getAttributeType(
-                value)))),
+            format: getVkFormat[compositeAttributeType(getAttributeType(value))](),
+            offset: uint32(i * sizeof(compositeAttributeType(getAttributeType(value)))),
           )
         )
-        location += nLocationSlots[compositeAttributeType(getAttributeType(
-            value))]()
+        location += nLocationSlots[compositeAttributeType(getAttributeType(value))]()
       binding += 1
--- a/src/semicongine/vulkan.nim	Mon Mar 06 23:50:21 2023 +0700
+++ b/src/semicongine/vulkan.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -25,6 +25,15 @@
 import ./vulkan/syncing
 export syncing
 
+import ./vulkan/shader
+export shader
+
+import ./vulkan/vertex
+export vertex
+
+import ./vulkan/pipeline
+export pipeline
+
 import ./vulkan/buffer
 export buffer
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/semicongine/vulkan/pipeline.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -0,0 +1,152 @@
+import ./api
+import ./utils
+import ./renderpass
+import ./vertex
+import ./device
+import ./shader
+
+type
+  Pipeline = object
+    device: Device
+    vk*: VkPipeline
+
+
+proc createPipeline*(renderPass: RenderPass, vertexShader: VertexShader, fragmentShader: FragmentShader): Pipeline =
+  assert renderPass.vk.valid
+  assert renderPass.device.vk.valid
+  result.device = renderPass.device
+
+  var descriptorType: VkDescriptorType
+  var bindingNumber = 0'u32
+  var arrayLen = 1
+  var shaderStage: seq[VkShaderStageFlagBits]
+  var layoutbinding = VkDescriptorSetLayoutBinding(
+    binding: bindingNumber,
+    descriptorType: descriptorType,
+    descriptorCount: uint32(arrayLen),
+    stageFlags: toBits shaderStage,
+    pImmutableSamplers: nil,
+  )
+  var descriptorLayoutBinding: seq[VkDescriptorSetLayoutBinding] = @[layoutbinding]
+  var layoutCreateInfo = VkDescriptorSetLayoutCreateInfo(
+    sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
+    bindingCount: uint32(descriptorLayoutBinding.len),
+    pBindings: descriptorLayoutBinding.toCPointer
+  )
+  var descriptorLayout: VkDescriptorSetLayout
+  checkVkResult vkCreateDescriptorSetLayout(
+    renderPass.device.vk,
+    addr(layoutCreateInfo),
+    nil,
+    addr(descriptorLayout),
+  )
+  var pushConstant = VkPushConstantRange(
+    stageFlags: toBits shaderStage,
+    offset: 0,
+    size: 0,
+  )
+
+  var descriptorSets: seq[VkDescriptorSetLayout] = @[descriptorLayout]
+  var pushConstants: seq[VkPushConstantRange] = @[pushConstant]
+  var pipelineLayoutInfo = VkPipelineLayoutCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
+      setLayoutCount: uint32(descriptorSets.len),
+      pSetLayouts: descriptorSets.toCPointer,
+      pushConstantRangeCount: uint32(pushConstants.len),
+      pPushConstantRanges: pushConstants.toCPointer,
+    )
+  var pipelineLayout: VkPipelineLayout
+  checkVkResult vkCreatePipelineLayout(renderPass.device.vk, addr(pipelineLayoutInfo), nil, addr(pipelineLayout))
+
+  var
+    vertexInputInfo = vertexShader.getVertexBindings()
+    inputAssembly = VkPipelineInputAssemblyStateCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
+      topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
+      primitiveRestartEnable: VK_FALSE,
+    )
+    viewportState = VkPipelineViewportStateCreateInfo(
+      sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
+      viewportCount: 1,
+      scissorCount: 1,
+    )
+    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: toBits [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: toBits [VK_COLOR_COMPONENT_R_BIT, VK_COLOR_COMPONENT_G_BIT, VK_COLOR_COMPONENT_B_BIT, 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],
+    )
+    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: dynamicStates.toCPointer,
+    )
+  var stages = @[vertexShader.getPipelineInfo(), fragmentShader.getPipelineInfo()]
+  var createInfo = VkGraphicsPipelineCreateInfo(
+    sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
+    stageCount: uint32(stages.len),
+    pStages: stages.toCPointer,
+    pVertexInputState: addr(vertexInputInfo),
+    pInputAssemblyState: addr(inputAssembly),
+    pViewportState: addr(viewportState),
+    pRasterizationState: addr(rasterizer),
+    pMultisampleState: addr(multisampling),
+    pDepthStencilState: nil,
+    pColorBlendState: addr(colorBlending),
+    pDynamicState: addr(dynamicState),
+    layout: pipelineLayout,
+    renderPass: renderPass.vk,
+    subpass: 0,
+    basePipelineHandle: VkPipeline(0),
+    basePipelineIndex: -1,
+  )
+  checkVkResult vkCreateGraphicsPipelines(
+    renderpass.device.vk,
+    VkPipelineCache(0),
+    1,
+    addr(createInfo),
+    nil,
+    addr(result.vk)
+  )
+
+proc destroy*(pipeline: var Pipeline) =
+  assert pipeline.device.vk.valid
+  assert pipeline.vk.valid
+
+  pipeline.device.vk.vkDestroyPipeline(pipeline.vk, nil)
+  pipeline.vk.reset()
--- a/src/semicongine/vulkan/renderpass.nim	Mon Mar 06 23:50:21 2023 +0700
+++ b/src/semicongine/vulkan/renderpass.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -15,7 +15,7 @@
     preserves: seq[uint32]
   RenderPass* = object
     vk*: VkRenderPass
-    device: Device
+    device*: Device
 
 proc createRenderPass*(
   device: Device,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/semicongine/vulkan/shader.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -0,0 +1,114 @@
+import std/os
+import std/hashes
+import std/strformat
+import std/compilesettings
+
+import ./api
+import ./device
+
+type
+  VertexShader*[VertexType] = object
+    device: Device
+    vertexType*: VertexType
+    module*: VkShaderModule
+  FragmentShader* = object
+    device: Device
+    module*: VkShaderModule
+
+proc staticExecChecked(command: string, input = ""): string {.compileTime.} =
+  let (output, exitcode) = gorgeEx(
+      command = command,
+      input = input)
+  if exitcode != 0:
+    raise newException(Exception, &"Running '{command}' produced exit code: {exitcode}" & output)
+  return output
+
+func stage2string(stage: VkShaderStageFlagBits): string {.compileTime.} =
+  case stage
+  of VK_SHADER_STAGE_VERTEX_BIT: "vert"
+  of VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT: "tesc"
+  of VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT: "tese"
+  of VK_SHADER_STAGE_GEOMETRY_BIT: "geom"
+  of VK_SHADER_STAGE_FRAGMENT_BIT: "frag"
+  of VK_SHADER_STAGE_COMPUTE_BIT: "comp"
+  else: ""
+
+proc compileGLSLToSPIRV(stage: static VkShaderStageFlagBits, shaderSource: static string, entrypoint: string): seq[uint32] {.compileTime.} =
+  when defined(nimcheck): # will not run if nimcheck is running
+    return result
+  const
+    stagename = stage2string(stage)
+    shaderHash = hash(shaderSource)
+    # cross compilation for windows workaround, sorry computer
+    shaderfile = getTempDir() / &"shader_{shaderHash}.{stagename}"
+    projectPath = querySetting(projectPath)
+
+  discard staticExecChecked(
+      command = &"{projectPath}/glslangValidator --entry-point {entrypoint} -V --stdin -S {stagename} -o {shaderfile}",
+      input = shaderSource
+  )
+
+  when defined(mingw) and defined(linux): # required for crosscompilation, path separators get messed up
+    let shaderbinary = staticRead shaderfile.replace("\\", "/")
+  else:
+    let shaderbinary = staticRead shaderfile
+  when defined(linux):
+    discard staticExecChecked(command = fmt"rm {shaderfile}")
+  elif defined(windows):
+    discard staticExecChecked(command = fmt"cmd.exe /c del {shaderfile}")
+  else:
+    raise newException(Exception, "Unsupported operating system")
+
+  var i = 0
+  while i < shaderbinary.len:
+    result.add(
+      (uint32(shaderbinary[i + 0]) shl 0) or
+      (uint32(shaderbinary[i + 1]) shl 8) or
+      (uint32(shaderbinary[i + 2]) shl 16) or
+      (uint32(shaderbinary[i + 3]) shl 24)
+    )
+    i += 4
+
+proc createVertexShader*[VertexType](device: Device, shader: static string, vertexType: VertexType, entryPoint: static string = "main"): VertexShader[VertexType] =
+  assert device.vk.valid
+
+  const constcode = compileGLSLToSPIRV(VK_SHADER_STAGE_VERTEX_BIT, shader, entryPoint)
+  var code = constcode
+  var createInfo = VkShaderModuleCreateInfo(
+    sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
+    codeSize: uint(code.len * sizeof(uint32)),
+    pCode: if code.len > 0: addr(code[0]) else: nil,
+  )
+  checkVkResult vkCreateShaderModule(device.vk, addr(createInfo), nil, addr(result.module))
+
+proc createFragmentShader*(device: Device, shader: static string, entryPoint: static string = "main"): FragmentShader =
+  assert device.vk.valid
+
+  const constcode = compileGLSLToSPIRV(VK_SHADER_STAGE_FRAGMENT_BIT, shader, entryPoint)
+  var code = constcode
+  var createInfo = VkShaderModuleCreateInfo(
+    sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
+    codeSize: uint(code.len * sizeof(uint32)),
+    pCode: if code.len > 0: addr(code[0]) else: nil,
+  )
+  checkVkResult vkCreateShaderModule(device.vk, addr(createInfo), nil, addr(result.module))
+
+proc getPipelineInfo*(shader: VertexShader|FragmentShader, entryPoint = "main"): VkPipelineShaderStageCreateInfo =
+  VkPipelineShaderStageCreateInfo(
+    sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
+    stage: VK_SHADER_STAGE_VERTEX_BIT,
+    module: shader.module,
+    pName: cstring(entryPoint),
+  )
+
+proc destroy*(shader: var VertexShader) =
+  assert shader.device.vk.valid
+  assert shader.module.valid
+  shader.device.vk.vkDestroyShaderModule(shader.module, nil)
+  shader.module.reset
+
+proc destroy*(shader: var FragmentShader) =
+  assert shader.device.vk.valid
+  assert shader.module.valid
+  shader.device.vk.vkDestroyShaderModule(shader.module, nil)
+  shader.module.reset
--- a/src/semicongine/vulkan/syncing.nim	Mon Mar 06 23:50:21 2023 +0700
+++ b/src/semicongine/vulkan/syncing.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -9,13 +9,13 @@
     vk*: VkFence
     device: Device
 
-proc createSemaphore(device: Device): Semaphore =
+proc createSemaphore*(device: Device): Semaphore =
   assert device.vk.valid
   var semaphoreInfo = VkSemaphoreCreateInfo(sType: VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO)
   result.device = device
   checkVkResult device.vk.vkCreateSemaphore(addr(semaphoreInfo), nil, addr(result.vk))
 
-proc createFence(device: Device): Fence =
+proc createFence*(device: Device): Fence =
   assert device.vk.valid
   var fenceInfo = VkFenceCreateInfo(
     sType: VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
@@ -24,13 +24,13 @@
   result.device = device
   checkVkResult device.vk.vkCreateFence(addr(fenceInfo), nil, addr(result.vk))
 
-proc destroy(semaphore: var Semaphore) =
+proc destroy*(semaphore: var Semaphore) =
   assert semaphore.device.vk.valid
   assert semaphore.vk.valid
   semaphore.device.vk.vkDestroySemaphore(semaphore.vk, nil)
   semaphore.vk.reset
 
-proc destroy(fence: var Fence) =
+proc destroy*(fence: var Fence) =
   assert fence.device.vk.valid
   assert fence.vk.valid
   fence.device.vk.vkDestroyFence(fence.vk, nil)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/semicongine/vulkan/vertex.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -0,0 +1,113 @@
+import std/tables
+import std/macros
+
+import ../math
+import ./api
+import ./utils
+import ./shader
+
+
+# add pragma to fields of the VertexType that represent per instance attributes
+template PerInstance*() {.pragma.}
+
+# from https://registry.khronos.org/vulkan/specs/1.3-extensions/html/chap15.html
+func nLocationSlots[T](value: T): uint32 =
+  when (T is TVec3[float64] or T is TVec3[uint64] or T is TVec4[float64] or T is TVec4[float64]):
+    return 2
+  elif T is SomeNumber or T is TVec:
+    return 1
+  else:
+    raise newException(Exception, "Unsupported vertex attribute type")
+
+# return the type into which larger types are divided
+func compositeAttribute[T](value: T): auto =
+  when T is TMat33[float32]:
+    Vec3()
+  elif T is TMat44[float32]:
+    Vec4()
+  else:
+    value
+
+# return the number of elements into which larger types are divided
+func compositeAttributesNumber[T](value: T): int =
+  when T is TMat33[float32]:
+    3
+  elif T is TMat44[float32]:
+    4
+  else:
+    1
+
+func getVkFormat[T](value: T): VkFormat =
+  when T is uint8: VK_FORMAT_R8_UINT
+  elif T is int8: VK_FORMAT_R8_SINT
+  elif T is uint16: VK_FORMAT_R16_UINT
+  elif T is int16: VK_FORMAT_R16_SINT
+  elif T is uint32: VK_FORMAT_R32_UINT
+  elif T is int32: VK_FORMAT_R32_SINT
+  elif T is uint64: VK_FORMAT_R64_UINT
+  elif T is int64: VK_FORMAT_R64_SINT
+  elif T is float32: VK_FORMAT_R32_SFLOAT
+  elif T is float64: VK_FORMAT_R64_SFLOAT
+  elif T is TVec2[uint8]: VK_FORMAT_R8G8_UINT
+  elif T is TVec2[int8]: VK_FORMAT_R8G8_SINT
+  elif T is TVec2[uint16]: VK_FORMAT_R16G16_UINT
+  elif T is TVec2[int16]: VK_FORMAT_R16G16_SINT
+  elif T is TVec2[uint32]: VK_FORMAT_R32G32_UINT
+  elif T is TVec2[int32]: VK_FORMAT_R32G32_SINT
+  elif T is TVec2[uint64]: VK_FORMAT_R64G64_UINT
+  elif T is TVec2[int64]: VK_FORMAT_R64G64_SINT
+  elif T is TVec2[float32]: VK_FORMAT_R32G32_SFLOAT
+  elif T is TVec2[float64]: VK_FORMAT_R64G64_SFLOAT
+  elif T is TVec3[uint8]: VK_FORMAT_R8G8B8_UINT
+  elif T is TVec3[int8]: VK_FORMAT_R8G8B8_SINT
+  elif T is TVec3[uint16]: VK_FORMAT_R16G16B16_UINT
+  elif T is TVec3[int16]: VK_FORMAT_R16G16B16_SINT
+  elif T is TVec3[uint32]: VK_FORMAT_R32G32B32_UINT
+  elif T is TVec3[int32]: VK_FORMAT_R32G32B32_SINT
+  elif T is TVec3[uint64]: VK_FORMAT_R64G64B64_UINT
+  elif T is TVec3[int64]: VK_FORMAT_R64G64B64_SINT
+  elif T is TVec3[float32]: VK_FORMAT_R32G32B32_SFLOAT
+  elif T is TVec3[float64]: VK_FORMAT_R64G64B64_SFLOAT
+  elif T is TVec4[uint8]: VK_FORMAT_R8G8B8A8_UINT
+  elif T is TVec4[int8]: VK_FORMAT_R8G8B8A8_SINT
+  elif T is TVec4[uint16]: VK_FORMAT_R16G16B16A16_UINT
+  elif T is TVec4[int16]: VK_FORMAT_R16G16B16A16_SINT
+  elif T is TVec4[uint32]: VK_FORMAT_R32G32B32A32_UINT
+  elif T is TVec4[int32]: VK_FORMAT_R32G32B32A32_SINT
+  elif T is TVec4[uint64]: VK_FORMAT_R64G64B64A64_UINT
+  elif T is TVec4[int64]: VK_FORMAT_R64G64B64A64_SINT
+  elif T is TVec4[float32]: VK_FORMAT_R32G32B32A32_SFLOAT
+  elif T is TVec4[float64]: VK_FORMAT_R64G64B64A64_SFLOAT
+  else: {.error: "Unsupported vertex attribute type".}
+
+proc getVertexBindings*(shader: VertexShader): VkPipelineVertexInputStateCreateInfo =
+  var location = 0'u32
+  var binding = 0'u32
+  var offset = 0'u32
+  var bindings: seq[VkVertexInputBindingDescription]
+  var attributes: seq[VkVertexInputAttributeDescription]
+
+  for name, value in shader.vertexType.fieldPairs:
+    bindings.add VkVertexInputBindingDescription(
+      binding: binding,
+      stride: uint32(sizeof(value)),
+      inputRate: if value.hasCustomPragma(PerInstance): VK_VERTEX_INPUT_RATE_INSTANCE else: VK_VERTEX_INPUT_RATE_VERTEX,
+    )
+    # allows to submit larger data structures like Mat44, for most other types will be 1
+    for i in 0 ..< compositeAttributesNumber(value):
+      attributes.add VkVertexInputAttributeDescription(
+        binding: binding,
+        location: location,
+        format: getVkFormat(compositeAttribute(value)),
+        offset: uint32(i * sizeof(compositeAttribute(value))),
+      )
+      location += nLocationSlots(compositeAttribute(value))
+    inc binding
+
+  return VkPipelineVertexInputStateCreateInfo(
+    sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
+    vertexBindingDescriptionCount: uint32(bindings.len),
+    pVertexBindingDescriptions: bindings.toCPointer,
+    vertexAttributeDescriptionCount: uint32(attributes.len),
+    pVertexAttributeDescriptions: attributes.toCPointer,
+  )
--- a/tests/test_vulkan_wrapper.nim	Mon Mar 06 23:50:21 2023 +0700
+++ b/tests/test_vulkan_wrapper.nim	Tue Mar 14 13:21:40 2023 +0700
@@ -4,6 +4,10 @@
 import semicongine/platform/window
 import semicongine/math
 
+type
+  Vertex = object
+    pos: Vec3
+
 
 when isMainModule:
   # print basic driver infos
@@ -64,13 +68,27 @@
     framebuffers.add device.createFramebuffer(renderpass, [imageview], swapchain.dimension)
 
   # todo: could be create inside "device", but it would be nice to have nim v2 with support for circular dependencies first
-  var commandPool = device.createCommandPool(family=device.firstGraphicsQueue().get().family, nBuffers=1)
+  var
+    commandPool = device.createCommandPool(family=device.firstGraphicsQueue().get().family, nBuffers=1)
+    imageAvailable = device.createSemaphore()
+    renderFinished = device.createSemaphore()
+    inflight = device.createFence()
+
+  var vertexshader = device.createVertexShader("#version 450\nvoid main() {}", Vertex())
+  var fragmentshader = device.createFragmentShader("#version 450\nvoid main() {}")
+  var pipeline = renderpass.createPipeline(vertexshader, fragmentshader)
 
   echo "All successfull"
   echo "Start cleanup"
 
+  # cleanup
+  pipeline.destroy()
+  vertexshader.destroy()
+  fragmentshader.destroy()
+  inflight.destroy()
+  imageAvailable.destroy()
+  renderFinished.destroy()
   commandPool.destroy()
-  # cleanup
   for fb in framebuffers.mitems:
     fb.destroy()
   renderpass.destroy()