changeset 189:df92519d4d68

add: initial code for texture support, not finished, had to completely refactor how to handle material-data (ie scene-wide data, sorry if you ever read this
author Sam <sam@basx.dev>
date Fri, 05 May 2023 23:45:50 +0700
parents de9dff24c422
children 8f2eaf0d2808
files src/semicongine/engine.nim src/semicongine/entity.nim src/semicongine/gpu_data.nim src/semicongine/renderer.nim src/semicongine/vulkan/descriptor.nim src/semicongine/vulkan/device.nim src/semicongine/vulkan/image.nim src/semicongine/vulkan/physicaldevice.nim src/semicongine/vulkan/pipeline.nim src/semicongine/vulkan/shader.nim tests/test_vulkan_wrapper.nim
diffstat 11 files changed, 272 insertions(+), 97 deletions(-) [+]
line wrap: on
line diff
--- a/src/semicongine/engine.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/engine.nim	Fri May 05 23:45:50 2023 +0700
@@ -102,12 +102,12 @@
   assert engine.state != Destroyed
   engine.renderer = engine.device.initRenderer(renderPass)
 
-proc addScene*(engine: var Engine, scene: Entity, vertexInput: seq[ShaderAttribute], transformAttribute="") =
+proc addScene*(engine: var Engine, scene: Scene, vertexInput: seq[ShaderAttribute], transformAttribute="") =
   assert engine.state != Destroyed
   assert transformAttribute == "" or transformAttribute in map(vertexInput, proc(a: ShaderAttribute): string = a.name)
   engine.renderer.setupDrawableBuffers(scene, vertexInput, transformAttribute=transformAttribute)
 
-proc renderScene*(engine: var Engine, scene: Entity) =
+proc renderScene*(engine: var Engine, scene: var Scene) =
   assert engine.state == Running
   assert engine.renderer.valid
   if engine.state == Running:
--- a/src/semicongine/entity.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/entity.nim	Fri May 05 23:45:50 2023 +0700
@@ -1,14 +1,22 @@
 import std/strformat
+import std/tables
 import std/hashes
 import std/typetraits
 
 import ./math/matrix
 import ./gpu_data
+import ./vulkan/image
 
 type
   Component* = ref object of RootObj
     entity*: Entity
 
+  Scene* = object
+    name*: string
+    root*: Entity
+    shaderGlobals*: Table[string, DataValue]
+    texture: Table[string, TextureImage]
+
   Entity* = ref object of RootObj
     name*: string
     transform*: Mat4 # todo: cache transform + only update VBO when transform changed
@@ -16,17 +24,31 @@
     children*: seq[Entity]
     components*: seq[Component]
 
-  ShaderGlobal* = ref object of Component
+  TextureImage* = ref object of RootObj
     name*: string
-    value*: DataValue
+    width*: int
+    height*: int
+    imagedata*: seq[array[4, uint8]]
 
-func `$`*(global: ShaderGlobal): string =
-  &"ShaderGlobal(name: {global.name}, {global.value})"
-
-func initShaderGlobal*[T](name: string, data: T): ShaderGlobal =
+func addShaderGlobal*[T](scene: var Scene, name: string, data: T) =
   var value = DataValue(thetype: getDataType[T]())
   value.setValue(data)
-  ShaderGlobal(name: name, value: value)
+  scene.shaderGlobals[name] = value
+
+func getShaderGlobal*(scene: Scene, name: string): DataValue =
+  return scene.shaderGlobals[name]
+
+func addTexture*[T](scene: var Scene, name: string, texture: Texture) =
+  scene.textures[name] = texture
+
+func newScene*(name: string, root: Entity): Scene =
+  Scene(name: name, root: root)
+
+func hash*(scene: Scene): Hash =
+  hash(scene.name)
+
+func `==`*(a, b: Scene): bool =
+  a.name == b.name
 
 func hash*(entity: Entity): Hash =
   hash(cast[pointer](entity))
--- a/src/semicongine/gpu_data.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/gpu_data.nim	Fri May 05 23:45:50 2023 +0700
@@ -6,7 +6,8 @@
 import ./math
 
 type
-  GPUType* = float32 | float64 | int8 | int16 | int32 | int64 | uint8 | uint16 | uint32 | uint64 | TVec2[int32] | TVec2[int64] | TVec3[int32] | TVec3[int64] | TVec4[int32] | TVec4[int64] | TVec2[uint32] | TVec2[uint64] | TVec3[uint32] | TVec3[uint64] | TVec4[uint32] | TVec4[uint64] | TVec2[float32] | TVec2[float64] | TVec3[float32] | TVec3[float64] | TVec4[float32] | TVec4[float64] | TMat2[float32] | TMat2[float64] | TMat23[float32] | TMat23[float64] | TMat32[float32] | TMat32[float64] | TMat3[float32] | TMat3[float64] | TMat34[float32] | TMat34[float64] | TMat43[float32] | TMat43[float64] | TMat4[float32] | TMat4[float64]
+  Sampler2DType* = object
+  GPUType* = float32 | float64 | int8 | int16 | int32 | int64 | uint8 | uint16 | uint32 | uint64 | TVec2[int32] | TVec2[int64] | TVec3[int32] | TVec3[int64] | TVec4[int32] | TVec4[int64] | TVec2[uint32] | TVec2[uint64] | TVec3[uint32] | TVec3[uint64] | TVec4[uint32] | TVec4[uint64] | TVec2[float32] | TVec2[float64] | TVec3[float32] | TVec3[float64] | TVec4[float32] | TVec4[float64] | TMat2[float32] | TMat2[float64] | TMat23[float32] | TMat23[float64] | TMat32[float32] | TMat32[float64] | TMat3[float32] | TMat3[float64] | TMat34[float32] | TMat34[float64] | TMat43[float32] | TMat43[float64] | TMat4[float32] | TMat4[float64] | Sampler2DType
   DataType* = enum
     Float32
     Float64
@@ -50,6 +51,7 @@
     Mat43F64
     Mat4F32
     Mat4F64
+    Sampler2D
   DataValue* = object
     case thetype*: DataType
     of Float32: float32: float32
@@ -94,6 +96,7 @@
     of Mat43F64: mat43f64: TMat43[float64]
     of Mat4F32: mat4f32: TMat4[float32]
     of Mat4F64: mat4f64: TMat4[float64]
+    of Sampler2D: discard
   DataList* = object
     len*: uint32
     case thetype*: DataType
@@ -139,6 +142,7 @@
     of Mat43F64: mat43f64: seq[TMat43[float64]]
     of Mat4F32: mat4f32*: seq[TMat4[float32]]
     of Mat4F64: mat4f64: seq[TMat4[float64]]
+    of Sampler2D: discard
   MemoryPerformanceHint* = enum
     PreferFastRead, PreferFastWrite
   ShaderAttribute* = object
@@ -157,7 +161,6 @@
     if attr.perInstance == false:
       result.add attr
 
-
 func numberOfVertexInputAttributeDescriptors*(thetype: DataType): uint32 =
   case thetype:
     of Mat2F32, Mat2F64, Mat23F32, Mat23F64: 2
@@ -209,6 +212,7 @@
     of Mat43F64: 92
     of Mat4F32: 64
     of Mat4F64: 128
+    of Sampler2D: 0
 
 func size*(attribute: ShaderAttribute, perDescriptor=false): uint32 =
   if perDescriptor: attribute.thetype.size div attribute.thetype.numberOfVertexInputAttributeDescriptors
@@ -273,6 +277,7 @@
   elif T is TMat43[float64]: Mat43F64
   elif T is TMat4[float32]: Mat4F32
   elif T is TMat4[float64]: Mat4F64
+  elif T is Sampler2DType: Sampler2D
   else:
     static:
       raise newException(Exception, &"Unsupported data type for GPU data: {name(T)}" )
@@ -338,6 +343,7 @@
   elif T is TMat43[float64]: value.mat43f64
   elif T is TMat4[float32]: value.mat4f32
   elif T is TMat4[float64]: value.mat4f64
+  else: {.error: "Virtual datatype has no value" .}
 
 func get*[T: GPUType|int|uint|float](value: DataList): seq[T] =
   when T is float32: value.float32
@@ -388,6 +394,7 @@
   elif T is TMat43[float64]: value.mat43f64
   elif T is TMat4[float32]: value.mat4f32
   elif T is TMat4[float64]: value.mat4f64
+  else: {. error: "Virtual datatype has no values" .}
 
 func getRawData*(value: var DataValue): (pointer, uint32) =
   result[1] = value.thetype.size
@@ -434,6 +441,7 @@
     of Mat43F64: result[0] = addr value.mat43f64
     of Mat4F32: result[0] = addr value.mat4f32
     of Mat4F64: result[0] = addr value.mat4f64
+    of Sampler2D: result[0] = nil
 
 func getRawData*(value: var DataList): (pointer, uint32) =
   result[1] = value.thetype.size * value.len
@@ -480,6 +488,7 @@
     of Mat43F64: result[0] = addr value.mat43f64[0]
     of Mat4F32: result[0] = addr value.mat4f32[0]
     of Mat4F64: result[0] = addr value.mat4f64[0]
+    of Sampler2D: result[0] = nil
 
 func initData*(value: var DataList, len: uint32) =
   value.len = len
@@ -526,6 +535,7 @@
     of Mat43F64: value.mat43f64.setLen(len)
     of Mat4F32: value.mat4f32.setLen(len)
     of Mat4F64: value.mat4f64.setLen(len)
+    of Sampler2D: discard
 
 func setValue*[T: GPUType|int|uint|float](value: var DataValue, data: T) =
   when T is float32: value.float32 = data
@@ -576,6 +586,7 @@
   elif T is TMat43[float64]: value.mat43f64 = data
   elif T is TMat4[float32]: value.mat4f32 = data
   elif T is TMat4[float64]: value.mat4f64 = data
+  else: {.error: "Virtual datatype has no value" .}
 
 func setValues*[T: GPUType|int|uint|float](value: var DataList, data: seq[T]) =
   value.len = uint32(data.len)
@@ -627,6 +638,7 @@
   elif T is TMat43[float64]: value.mat43f64 = data
   elif T is TMat4[float32]: value.mat4f32 = data
   elif T is TMat4[float64]: value.mat4f64 = data
+  else: {. error: "Virtual datatype has no values" .}
 
 func setValue*[T: GPUType|int|uint|float](value: var DataList, i: uint32, data: T) =
   assert i < value.len
@@ -678,6 +690,7 @@
   elif T is TMat43[float64]: value.mat43f64[i] = data
   elif T is TMat4[float32]: value.mat4f32[i] = data
   elif T is TMat4[float64]: value.mat4f64[i] = data
+  else: {. error: "Virtual datatype has no values" .}
 
 const TYPEMAP = {
     Float32: VK_FORMAT_R32_SFLOAT,
@@ -780,6 +793,7 @@
     of Mat43F64: 2
     of Mat4F32: 1
     of Mat4F64: 2
+    of Sampler2D: 1
 
 func glslType*(thetype: DataType): string =
   # todo: likely not correct as we would need to enable some 
@@ -823,6 +837,7 @@
     of Mat43F64: "dmat43"
     of Mat4F32: "mat4"
     of Mat4F64: "dmat4"
+    of Sampler2D: "sampler2D"
 
 func glslInput*(group: seq[ShaderAttribute]): seq[string] =
   if group.len == 0:
@@ -833,7 +848,7 @@
       for j in 0 ..< attribute.thetype.numberOfVertexInputAttributeDescriptors:
         i += attribute.thetype.nLocationSlots
 
-func glslUniforms*(group: seq[ShaderAttribute], blockName="Uniforms", binding=0): seq[string] =
+func glslUniforms*(group: seq[ShaderAttribute], blockName="Uniforms", binding: int): seq[string] =
   if group.len == 0:
     return @[]
   # currently only a single uniform block supported, therefore binding = 0
@@ -842,6 +857,14 @@
     result.add(&"    {attribute.thetype.glslType} {attribute.name};")
   result.add(&"}} {blockName};")
 
+func glslSamplers*(group: seq[ShaderAttribute], basebinding: int): seq[string] =
+  if group.len == 0:
+    return @[]
+  var thebinding = basebinding
+  for attribute in group:
+    result.add(&"layout(binding = {thebinding}) uniform {attribute.thetype.glslType} {attribute.name};")
+    inc thebinding
+
 func glslOutput*(group: seq[ShaderAttribute]): seq[string] =
   if group.len == 0:
     return @[]
--- a/src/semicongine/renderer.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/renderer.nim	Fri May 05 23:45:50 2023 +0700
@@ -11,6 +11,8 @@
 import ./vulkan/physicaldevice
 import ./vulkan/renderpass
 import ./vulkan/swapchain
+import ./vulkan/descriptor
+import ./vulkan/image
 
 import ./entity
 import ./mesh
@@ -22,6 +24,9 @@
     drawables*: OrderedTable[Mesh, Drawable]
     vertexBuffers*: Table[MemoryPerformanceHint, Buffer]
     indexBuffer*: Buffer
+    uniformBuffers*: seq[Buffer] # one per frame-in-flight
+    images*: seq[Image] # used to back texturees
+    textures*: seq[Table[string, Texture]] # per frame-in-flight
     attributeLocation*: Table[string, MemoryPerformanceHint]
     attributeBindingNumber*: Table[string, int]
     transformAttribute: string # name of attribute that is used for per-instance mesh transformation
@@ -31,7 +36,7 @@
     surfaceFormat: VkSurfaceFormatKHR
     renderPass: RenderPass
     swapchain: Swapchain
-    scenedata: Table[Entity, SceneData]
+    scenedata: Table[Scene, SceneData]
 
 
 proc initRenderer*(device: Device, renderPass: RenderPass): Renderer =
@@ -47,10 +52,11 @@
     raise newException(Exception, "Unable to create swapchain")
   result.swapchain = swapchain.get()
 
-proc setupDrawableBuffers*(renderer: var Renderer, scene: Entity, inputs: seq[ShaderAttribute], transformAttribute="") =
+proc setupDrawableBuffers*(renderer: var Renderer, scene: Scene, inputs: seq[ShaderAttribute], transformAttribute="") =
   assert not (scene in renderer.scenedata)
   var data = SceneData()
 
+  # when mesh transformation are handled through the scenegraph-transformation, set it up here
   if transformattribute != "":
     var hasTransformAttribute = false
     for input in inputs:
@@ -61,14 +67,15 @@
     assert hasTransformAttribute
     data.transformAttribute = transformAttribute
 
-
+  # find all meshes, populate missing attribute values for shader
   var allMeshes: seq[Mesh]
-  for mesh in allComponentsOfType[Mesh](scene):
+  for mesh in allComponentsOfType[Mesh](scene.root):
     allMeshes.add mesh
     for inputAttr in inputs:
       if not mesh.hasDataFor(inputAttr.name):
         mesh.initData(inputAttr)
   
+  # create index buffer if necessary
   var indicesBufferSize = 0'u64
   for mesh in allMeshes:
     if mesh.indexType != None:
@@ -89,7 +96,8 @@
       preferVRAM=true,
     )
 
-  # one vertex data buffer per memory location
+  # create vertex buffers and calculcate offsets
+  # trying to use one buffer per memory type
   var
     perLocationOffsets: Table[MemoryPerformanceHint, uint64]
     perLocationSizes: Table[MemoryPerformanceHint, uint64]
@@ -113,6 +121,7 @@
       )
       perLocationOffsets[memoryPerformanceHint] = 0
 
+  # fill vertex buffers
   var indexBufferOffset = 0'u64
   for mesh in allMeshes:
     var offsets: seq[(string, MemoryPerformanceHint, uint64)]
@@ -145,6 +154,28 @@
       indexBufferOffset += size
     data.drawables[mesh] = drawable
 
+  # setup uniforms and textures
+  for i in 0 ..< renderer.renderPass.subpasses.len:
+    var subpass = renderer.renderPass.subpasses[i]
+    for pipeline in subpass.pipelines.mitems:
+      var uniformBufferSize = 0'u64
+      for uniform in pipeline.uniforms:
+        uniformBufferSize += uniform.thetype.size
+      if uniformBufferSize > 0:
+        for i in 0 ..< renderer.swapchain.inFlightFrames:
+          data.uniformBuffers.add renderer.device.createBuffer(
+            size=uniformBufferSize,
+            usage=[VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT],
+            requireMappable=true,
+            preferVRAM=true,
+          )
+      # textures: seq[Table[string, Texture]]
+      var textures: Table[string, Texture]
+      data.textures.add textures
+      # need a separate descriptor for each frame in flight
+      pipeline.setupDescriptors(data.uniformBuffers, data.textures, inFlightFrames=renderer.swapchain.inFlightFrames)
+      pipeline.descriptorSets[i].writeDescriptorSet()
+
   renderer.scenedata[scene] = data
 
 proc refreshMeshAttributeData(sceneData: var SceneData, mesh: Mesh, attribute: string) =
@@ -154,11 +185,10 @@
   let bindingNumber = sceneData.attributeBindingNumber[attribute]
   sceneData.vertexBuffers[memoryPerformanceHint].setData(pdata, size, sceneData.drawables[mesh].bufferOffsets[bindingNumber][2])
 
-
-proc refreshMeshData*(renderer: var Renderer, scene: Entity) =
+proc refreshMeshData*(renderer: var Renderer, scene: Scene) =
   assert scene in renderer.scenedata
 
-  for mesh in allComponentsOfType[Mesh](scene):
+  for mesh in allComponentsOfType[Mesh](scene.root):
     # if mesh transformation attribute is enabled, update the model matrix
     if renderer.scenedata[scene].transformAttribute != "":
       let transform = mesh.entity.getModelTransform()
@@ -173,9 +203,24 @@
     var m = mesh
     m.clearDataChanged()
 
-
+proc updateUniforms(renderer: Renderer, scene: var Scene, currentInFlight: int) =
+  assert scene in renderer.scenedata
+  var data = renderer.scenedata[scene]
+  if data.uniformBuffers.len == 0:
+    return
+  assert data.uniformBuffers[currentInFlight].vk.valid
 
-proc render*(renderer: var Renderer, scene: Entity) =
+  for i in 0 ..< renderer.renderPass.subpasses.len:
+    var subpass = renderer.renderPass.subpasses[i]
+    for pipeline in subpass.pipelines.mitems:
+      var offset = 0'u64
+      for uniform in pipeline.uniforms:
+        assert uniform.thetype == scene.shaderGlobals[uniform.name].thetype
+        let (pdata, size) = scene.shaderGlobals[uniform.name].getRawData()
+        data.uniformBuffers[currentInFlight].setData(pdata, size, offset)
+        offset += size
+
+proc render*(renderer: var Renderer, scene: var Scene) =
   assert scene in renderer.scenedata
 
   var
@@ -196,13 +241,14 @@
 
   commandBuffer.beginRenderCommands(renderer.renderPass, renderer.swapchain.currentFramebuffer())
 
+  renderer.updateUniforms(scene, renderer.swapchain.currentInFlight)
+
   for i in 0 ..< renderer.renderPass.subpasses.len:
     let subpass = renderer.renderPass.subpasses[i]
     for pipeline in subpass.pipelines:
       var mpipeline = pipeline
       commandBuffer.vkCmdBindPipeline(subpass.pipelineBindPoint, mpipeline.vk)
       commandBuffer.vkCmdBindDescriptorSets(subpass.pipelineBindPoint, mpipeline.layout, 0, 1, addr(mpipeline.descriptorSets[renderer.swapchain.currentInFlight].vk), 0, nil)
-      mpipeline.updateUniforms(scene, renderer.swapchain.currentInFlight)
 
       debug "Scene buffers:"
       for (location, buffer) in renderer.scenedata[scene].vertexBuffers.pairs:
@@ -229,7 +275,6 @@
     checkVkResult renderer.device.vk.vkDeviceWaitIdle()
     oldSwapchain.destroy()
 
-
 func framesRendered*(renderer: Renderer): uint64 =
   renderer.swapchain.framesRendered
 
@@ -239,8 +284,13 @@
 proc destroy*(renderer: var Renderer) =
   for data in renderer.scenedata.mvalues:
     for buffer in data.vertexBuffers.mvalues:
+      assert buffer.vk.valid
       buffer.destroy()
     if data.indexBuffer.vk.valid:
+      assert data.indexBuffer.vk.valid
       data.indexBuffer.destroy()
+    for buffer in data.uniformBuffers.mitems:
+      assert buffer.vk.valid
+      buffer.destroy()
   renderer.renderPass.destroy()
   renderer.swapchain.destroy()
--- a/src/semicongine/vulkan/descriptor.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/vulkan/descriptor.nim	Fri May 05 23:45:50 2023 +0700
@@ -1,29 +1,48 @@
 import std/enumerate
+import std/tables
 
 import ./api
 import ./device
 import ./buffer
 import ./utils
+import ./image
 
 type
+  DescriptorType* = enum
+    Uniform, ImageSampler
   Descriptor* = object # "fields" of a DescriptorSetLayout
-    thetype*: VkDescriptorType
+    name*: string
     count*: uint32
     stages*: seq[VkShaderStageFlagBits]
     itemsize*: uint32
+    case thetype*: DescriptorType
+    of Uniform:
+      buffer*: Buffer
+      offset*: uint64
+      size*: uint64
+    of ImageSampler:
+      imageview*: ImageView
+      sampler*: Sampler
+  DescriptorSet* = object # "instance" of a DescriptorSetLayout
+    vk*: VkDescriptorSet
+    layout*: DescriptorSetLayout
   DescriptorSetLayout* = object # "type-description" of a DescriptorSet
     device: Device
     vk*: VkDescriptorSetLayout
     descriptors*: seq[Descriptor]
-  DescriptorSet* = object # "instance" of a DescriptorSetLayout
-    vk*: VkDescriptorSet
-    layout: DescriptorSetLayout
   DescriptorPool* = object # required for allocation of DescriptorSet
     device: Device
     vk*: VkDescriptorPool
     maxSets*: uint32 # maximum number of allocatable descriptor sets
     counts*: seq[(VkDescriptorType, uint32)] # maximum number for each descriptor type to allocate
 
+const DESCRIPTOR_TYPE_MAP = {
+  Uniform: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+  ImageSampler: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+}.toTable
+
+func vkType(descriptor: Descriptor): VkDescriptorType =
+  DESCRIPTOR_TYPE_MAP[descriptor.thetype]
 
 proc createDescriptorSetLayout*(device: Device, descriptors: seq[Descriptor]): DescriptorSetLayout =
   assert device.vk.valid
@@ -35,7 +54,7 @@
   for i, descriptor in enumerate(descriptors):
     layoutbindings.add VkDescriptorSetLayoutBinding(
       binding: uint32(i),
-      descriptorType: descriptor.thetype,
+      descriptorType: descriptor.vkType,
       descriptorCount: descriptor.count,
       stageFlags: toBits descriptor.stages,
       pImmutableSamplers: nil,
@@ -105,29 +124,52 @@
   for descriptorSet in descriptorSets:
     result.add DescriptorSet(vk: descriptorSet, layout: layout)
 
-proc setDescriptorSet*(descriptorSet: DescriptorSet, buffer: Buffer, bindingBase=0'u32) =
+proc writeDescriptorSet*(descriptorSet: DescriptorSet, bindingBase=0'u32) =
   # assumes descriptors of the descriptorSet are arranged interleaved in buffer
   assert descriptorSet.layout.device.vk.valid
   assert descriptorSet.layout.vk.valid
   assert descriptorSet.vk.valid
-  assert buffer.device.vk.valid
-  assert buffer.vk.valid
 
   var descriptorSetWrites: seq[VkWriteDescriptorSet]
+  var bufferInfos: seq[VkDescriptorBufferInfo]
+  var imageInfos: seq[VkDescriptorImageInfo]
 
-  var offset = VkDeviceSize(0)
   var i = bindingBase
   for descriptor in descriptorSet.layout.descriptors:
-    let length = VkDeviceSize(descriptor.itemsize * descriptor.count)
-    var bufferInfo = VkDescriptorBufferInfo(buffer: buffer.vk, offset: offset, range: length)
-    descriptorSetWrites.add VkWriteDescriptorSet(
-        sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
-        dstSet: descriptorSet.vk,
-        dstBinding: i,
-        dstArrayElement: 0,
-        descriptorType: descriptor.thetype,
-        descriptorCount: descriptor.count,
-        pBufferInfo: addr(bufferInfo),
+    if descriptor.thetype == Uniform:
+      assert descriptor.buffer.vk.valid
+      bufferInfos.add VkDescriptorBufferInfo(
+        buffer: descriptor.buffer.vk,
+        offset: descriptor.offset,
+        range: descriptor.size,
       )
-    offset += length
+      descriptorSetWrites.add VkWriteDescriptorSet(
+          sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+          dstSet: descriptorSet.vk,
+          dstBinding: i,
+          dstArrayElement: 0,
+          descriptorType: descriptor.vkType,
+          descriptorCount: descriptor.count,
+          pBufferInfo: addr bufferInfos[^1],
+        )
+    #[ 
+    elif descriptor.thetype == ImageSampler:
+      assert descriptor.imageview.vk.valid
+      assert descriptor.sampler.vk.valid
+      imageInfos.add VkDescriptorImageInfo(
+        imageLayout: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+        imageView: descriptor.imageview.vk,
+        sampler: descriptor.sampler.vk,
+      )
+      descriptorSetWrites.add VkWriteDescriptorSet(
+          sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+          dstSet: descriptorSet.vk,
+          dstBinding: i,
+          dstArrayElement: 0,
+          descriptorType: descriptor.vkType,
+          descriptorCount: descriptor.count,
+          pImageInfo: addr imageInfos[^1],
+        )
+    ]#
+    inc i
   descriptorSet.layout.device.vk.vkUpdateDescriptorSets(uint32(descriptorSetWrites.len), descriptorSetWrites.toCPointer, 0, nil)
--- a/src/semicongine/vulkan/device.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/vulkan/device.nim	Fri May 05 23:45:50 2023 +0700
@@ -20,7 +20,7 @@
     graphics: bool
 
 proc `$`*(device: Device): string =
-  "vk: " & $device.vk & "physicalDevice: " & $device.physicalDevice
+  "Device: vk=" & $device.vk
 
 proc createDevice*(
   instance: Instance,
--- a/src/semicongine/vulkan/image.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/vulkan/image.nim	Fri May 05 23:45:50 2023 +0700
@@ -22,9 +22,15 @@
       of false: discard
       of true:
         memory*: DeviceMemory
+  Sampler* = object
+    device*: Device
+    vk*: VkSampler
   ImageView* = object
     vk*: VkImageView
     image*: Image
+  Texture* = object
+    imageView*: ImageView
+    sampler*: Sampler
 
 const DEPTH_FORMAT_MAP = {
   PixelDepth(1): VK_FORMAT_R8_SRGB,
@@ -132,10 +138,15 @@
     )
 
 # currently only usable for texture access from shader
-proc createImage(device: Device, width, height: uint32, depth: PixelDepth, data: pointer): Image =
+proc createImage*(device: Device, width, height: uint32, depth: PixelDepth, data: pointer): Image =
   assert device.vk.valid
+  assert width > 0
+  assert height > 0
+  assert depth != 2
+  assert data != nil
 
   let size = width * height * depth
+  result.device = device
   result.width = width
   result.height = height
   result.depth = depth
@@ -156,6 +167,7 @@
     samples: VK_SAMPLE_COUNT_1_BIT,
   )
   checkVkResult device.vk.vkCreateImage(addr imageInfo, nil, addr result.vk)
+  result.allocateMemory(requireMappable=false, preferVRAM=true, preferAutoFlush=false)
   result.transitionImageLayout(VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
 
   var stagingBuffer = device.createBuffer(size=size, usage=[VK_BUFFER_USAGE_TRANSFER_SRC_BIT], requireMappable=true, preferVRAM=false, preferAutoFlush=true)
@@ -174,7 +186,8 @@
     image.memoryAllocated = false
   image.vk.reset
 
-proc createSampler(device: Device): VkSampler =
+proc createSampler*(device: Device): Sampler =
+  assert device.vk.valid
   var samplerInfo = VkSamplerCreateInfo(
     sType: VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
     magFilter: VK_FILTER_LINEAR,
@@ -193,8 +206,14 @@
     minLod: 0,
     maxLod: 0,
   )
-  checkVkResult device.vk.vkCreateSampler(addr samplerInfo, nil, addr result)
+  result.device = device
+  checkVkResult device.vk.vkCreateSampler(addr samplerInfo, nil, addr result.vk)
 
+proc destroy*(sampler: var Sampler) =
+  assert sampler.device.vk.valid
+  assert sampler.vk.valid
+  sampler.device.vk.vkDestroySampler(sampler.vk, nil)
+  sampler.vk.reset
 
 proc createImageView*(
   image: Image,
@@ -234,3 +253,14 @@
   assert imageview.vk.valid
   imageview.image.device.vk.vkDestroyImageView(imageview.vk, nil)
   imageview.vk.reset()
+
+proc createTexture*(image: Image): Texture =
+  assert image.vk.valid
+  assert image.device.vk.valid
+
+  result.imageView = image.createImageView()
+  result.sampler = image.device.createSampler()
+
+proc destroy*(texture: var Texture) =
+  texture.imageView.destroy()
+  texture.sampler.destroy()
--- a/src/semicongine/vulkan/physicaldevice.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/vulkan/physicaldevice.nim	Fri May 05 23:45:50 2023 +0700
@@ -20,6 +20,9 @@
     index*: uint32
     flags*: seq[VkQueueFlagBits]
 
+func `$`*(device: PhysicalDevice): string =
+  "Physical device: vk=" & $device.vk & ", name=" & $device.name & ", devicetype=" & $device.devicetype
+
 proc getPhysicalDevices*(instance: Instance): seq[PhysicalDevice] =
   assert instance.vk.valid
   assert instance.surface.valid
--- a/src/semicongine/vulkan/pipeline.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/vulkan/pipeline.nim	Fri May 05 23:45:50 2023 +0700
@@ -7,8 +7,8 @@
 import ./shader
 import ./buffer
 import ./utils
+import ./image
 
-import ../entity
 import ../gpu_data
 
 type
@@ -20,7 +20,6 @@
     descriptorSetLayout*: DescriptorSetLayout
     descriptorPool*: DescriptorPool
     descriptorSets*: seq[DescriptorSet]
-    uniformBuffers: seq[Buffer]
 
 func inputs*(pipeline: Pipeline): seq[ShaderAttribute] =
   for shader in pipeline.shaders:
@@ -37,24 +36,23 @@
         uniformList[attribute.name] = attribute
   result = uniformList.values.toSeq
 
-proc setupUniforms(pipeline: var Pipeline, inFlightFrames: int) =
+proc setupDescriptors*(pipeline: var Pipeline, buffers: seq[Buffer], textures: seq[Table[string, Texture]], inFlightFrames: int) =
   assert pipeline.vk.valid
-
-  var uniformBufferSize = 0'u64
-  for uniform in pipeline.uniforms:
-    uniformBufferSize += uniform.thetype.size
-  if uniformBufferSize == 0:
-    return
+  assert buffers.len == 0 or buffers.len == inFlightFrames
+  # assert textures.len == 0 or textures.len == inFlightFrames
 
   for i in 0 ..< inFlightFrames:
-    var buffer = pipeline.device.createBuffer(
-      size=uniformBufferSize,
-      usage=[VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT],
-      requireMappable=true,
-      preferVRAM=true,
-    )
-    pipeline.uniformBuffers.add buffer
-    pipeline.descriptorSets[i].setDescriptorSet(buffer)
+    var offset = 0'u64
+    for descriptor in pipeline.descriptorSets[i].layout.descriptors.mitems:
+      if descriptor.thetype == Uniform:
+        let size = VkDeviceSize(descriptor.itemsize * descriptor.count)
+        descriptor.buffer = buffers[i]
+        descriptor.offset = offset
+        descriptor.size = size
+        offset += size
+      # elif descriptor.thetype == ImageSampler:
+        # descriptor.imageview = textures[i][descriptor.name].imageView
+        # descriptor.sampler = textures[i][descriptor.name].sampler
 
 proc createPipeline*(device: Device, renderPass: VkRenderPass, vertexCode: ShaderCode, fragmentCode: ShaderCode, inFlightFrames: int, subpass = 0'u32): Pipeline =
   assert renderPass.valid
@@ -67,18 +65,28 @@
   assert fragmentShader.stage == VK_SHADER_STAGE_FRAGMENT_BIT
   assert vertexShader.outputs == fragmentShader.inputs
   assert vertexShader.uniforms == fragmentShader.uniforms
+  assert vertexShader.samplers == fragmentShader.samplers
 
   result.device = device
   result.shaders = @[vertexShader, fragmentShader]
   
   var descriptors = @[
     Descriptor(
-      thetype: VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
+      name: "Uniforms",
+      thetype: Uniform,
       count: 1,
       stages: @[VK_SHADER_STAGE_VERTEX_BIT, VK_SHADER_STAGE_FRAGMENT_BIT],
       itemsize: vertexShader.uniforms.size(),
     ),
   ]
+  for sampler in vertexShader.samplers:
+    descriptors.add Descriptor(
+      name: sampler.name,
+      thetype: ImageSampler,
+      count: 1,
+      stages: @[VK_SHADER_STAGE_VERTEX_BIT, VK_SHADER_STAGE_FRAGMENT_BIT],
+      itemsize: 0,
+    )
   result.descriptorSetLayout = device.createDescriptorSetLayout(descriptors)
 
   # TODO: Push constants
@@ -183,27 +191,13 @@
     nil,
     addr(result.vk)
   )
-  result.descriptorPool = result.device.createDescriptorSetPool(@[(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, uint32(inFlightFrames))])
+  var poolsizes = @[(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, uint32(inFlightFrames))]
+  if vertexShader.samplers.len > 0:
+    poolsizes.add (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, uint32(inFlightFrames * vertexShader.samplers.len))
+
+  result.descriptorPool = result.device.createDescriptorSetPool(poolsizes)
   result.descriptorSets = result.descriptorPool.allocateDescriptorSet(result.descriptorSetLayout, inFlightFrames)
   discard result.uniforms # just for assertion
-  result.setupUniforms(inFlightFrames=inFlightFrames)
-
-proc updateUniforms*(pipeline: Pipeline, rootEntity: Entity, currentInFlight: int) =
-  if pipeline.uniformBuffers.len == 0:
-    return
-  assert pipeline.vk.valid
-  assert pipeline.uniformBuffers[currentInFlight].vk.valid
-
-  var globalsByName: Table[string, DataValue]
-  for component in allComponentsOfType[ShaderGlobal](rootEntity):
-    globalsByName[component.name] = component.value
-
-  var offset = 0'u64
-  for uniform in pipeline.uniforms:
-    assert uniform.thetype == globalsByName[uniform.name].thetype
-    let (pdata, size) = globalsByName[uniform.name].getRawData()
-    pipeline.uniformBuffers[currentInFlight].setData(pdata, size, offset)
-    offset += size
 
 
 proc destroy*(pipeline: var Pipeline) =
@@ -212,10 +206,6 @@
   assert pipeline.layout.valid
   assert pipeline.descriptorSetLayout.vk.valid
 
-  for buffer in pipeline.uniformBuffers.mitems:
-    assert buffer.vk.valid
-    buffer.destroy()
-  
   if pipeline.descriptorPool.vk.valid:
     pipeline.descriptorPool.destroy()
 
--- a/src/semicongine/vulkan/shader.nim	Thu May 04 23:44:15 2023 +0700
+++ b/src/semicongine/vulkan/shader.nim	Fri May 05 23:45:50 2023 +0700
@@ -26,6 +26,7 @@
     binary: seq[uint32]
     inputs*: seq[ShaderAttribute]
     uniforms*: seq[ShaderAttribute]
+    samplers*: seq[ShaderAttribute]
     outputs*: seq[ShaderAttribute]
   Shader* = object
     device: Device
@@ -34,6 +35,7 @@
     entrypoint*: string
     inputs*: seq[ShaderAttribute]
     uniforms*: seq[ShaderAttribute]
+    samplers*: seq[ShaderAttribute]
     outputs*: seq[ShaderAttribute]
 
 
@@ -88,6 +90,7 @@
   stage: VkShaderStageFlagBits,
   inputs: seq[ShaderAttribute]= @[],
   uniforms: seq[ShaderAttribute]= @[],
+  samplers: seq[ShaderAttribute]= @[],
   outputs: seq[ShaderAttribute]= @[],
   version=DEFAULT_SHADER_VERSION ,
   entrypoint=DEFAULT_SHADER_ENTRYPOINT ,
@@ -97,13 +100,15 @@
   var code = @[&"#version {version}", ""] &
   # var code = @[&"#version {version}", "layout(row_major) uniform;", ""] &
     (if inputs.len > 0: inputs.glslInput() & @[""] else: @[]) &
-    (if uniforms.len > 0: uniforms.glslUniforms() & @[""] else: @[]) &
+    (if uniforms.len > 0: uniforms.glslUniforms(binding=0) & @[""] else: @[]) &
+    (if samplers.len > 0: samplers.glslSamplers(basebinding=1) & @[""] else: @[]) &
     (if outputs.len > 0: outputs.glslOutput() & @[""] else: @[]) &
     @[&"void {entrypoint}(){{"] &
     main &
     @[&"}}"]
   result.inputs = inputs
   result.uniforms = uniforms
+  result.samplers = samplers
   result.outputs = outputs
   result.entrypoint = entrypoint
   result.stage = stage
@@ -114,12 +119,13 @@
   stage: VkShaderStageFlagBits,
   inputs: seq[ShaderAttribute]= @[],
   uniforms: seq[ShaderAttribute]= @[],
+  samplers: seq[ShaderAttribute]= @[],
   outputs: seq[ShaderAttribute]= @[],
   version=DEFAULT_SHADER_VERSION ,
   entrypoint=DEFAULT_SHADER_ENTRYPOINT ,
   main: string
 ): ShaderCode {.compileTime.} =
-  return compileGlslShader(stage, inputs, uniforms, outputs, version, entrypoint, @[main])
+  return compileGlslShader(stage, inputs, uniforms, samplers, outputs, version, entrypoint, @[main])
 
 
 proc createShaderModule*(
@@ -132,6 +138,7 @@
   result.device = device
   result.inputs = shaderCode.inputs
   result.uniforms = shaderCode.uniforms
+  result.samplers = shaderCode.samplers
   result.outputs = shaderCode.outputs
   result.entrypoint = shaderCode.entrypoint
   result.stage = shaderCode.stage
--- a/tests/test_vulkan_wrapper.nim	Thu May 04 23:44:15 2023 +0700
+++ b/tests/test_vulkan_wrapper.nim	Fri May 05 23:45:50 2023 +0700
@@ -117,11 +117,13 @@
     ]
     vertexOutput = @[attr[Vec3f]("outcolor")]
     uniforms = @[attr[float32]("time")]
+    # samplers = @[attr[Sampler2DType]("my_little_texture")]
     fragOutput = @[attr[Vec4f]("color")]
     vertexCode = compileGlslShader(
       stage=VK_SHADER_STAGE_VERTEX_BIT,
       inputs=vertexInput,
       uniforms=uniforms,
+      # samplers=samplers,
       outputs=vertexOutput,
       main="""gl_Position = vec4(position + translate, 1.0); outcolor = color;"""
     )
@@ -129,28 +131,34 @@
       stage=VK_SHADER_STAGE_FRAGMENT_BIT,
       inputs=vertexOutput,
       uniforms=uniforms,
+      # samplers=samplers,
       outputs=fragOutput,
-      main="color = vec4(outcolor, 0.8);"
+      # main="color = texture(my_little_texture, outcolor.xy) * 0.5 + vec4(outcolor, 1) * 0.5;"
+      main="color = vec4(outcolor, 1) * 0.5;"
     )
   var renderPass = engine.gpuDevice.simpleForwardRenderPass(vertexCode, fragmentCode)
   engine.setRenderer(renderPass)
 
   # INIT SCENES
-  var scenes = [scene_simple(), scene_different_mesh_types(), scene_primitives()]
-  var time = initShaderGlobal("time", 0.0'f32)
+  var scenes = [
+    newScene("simple", scene_simple()),
+    newScene("different mesh types", scene_different_mesh_types()),
+    newScene("primitives", scene_primitives())
+  ]
   for scene in scenes.mitems:
-    scene.components.add time
+    scene.addShaderGlobal("time", 0.0'f32)
     engine.addScene(scene, vertexInput)
 
   # MAINLOOP
   echo "Setup successfull, start rendering"
   for i in 0 ..< 3:
-    for scene in scenes:
+    for scene in scenes.mitems:
       for i in 0 ..< 1000:
         if engine.updateInputs() != Running or engine.keyIsDown(Escape):
           engine.destroy()
           return
-        setValue[float32](time.value, get[float32](time.value) + 0.0005)
+        var time = scene.getShaderGlobal("time")
+        setValue[float32](time, get[float32](time) + 0.0005)
         engine.renderScene(scene)
   echo "Rendered ", engine.framesRendered, " frames"
   echo "Processed ", engine.eventsProcessed, " events"