changeset 1210:570ac1620fb6 compiletime-tests

fix: make uniform-block-arrays working
author sam <sam@basx.dev>
date Wed, 17 Jul 2024 00:30:49 +0700
parents e177336cac3f
children d9799f74f5a7
files semicongine/rendering.nim semicongine/rendering/renderer.nim semicongine/rendering/shaders.nim tests/test_rendering tests/test_rendering.nim
diffstat 5 files changed, 255 insertions(+), 145 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/rendering.nim	Tue Jul 16 20:39:35 2024 +0700
+++ b/semicongine/rendering.nim	Wed Jul 17 00:30:49 2024 +0700
@@ -104,17 +104,18 @@
     rawPointer: pointer # if not nil, buffer is using mapped memory
     offsetNextFree: uint64
   Texture*[T: TextureType] = object
+    width*: uint32
+    height*: uint32
+    interpolation*: VkFilter = VK_FILTER_LINEAR
+    data*: seq[T]
     vk: VkImage
     imageview: VkImageView
     sampler: VkSampler
-    width*: uint32
-    height*: uint32
-    data*: seq[T]
   GPUArray*[T: SupportedGPUType, TBuffer: static BufferType] = object
     data*: seq[T]
     buffer*: Buffer
     offset*: uint64
-  GPUValue*[T: object|array, TBuffer: static BufferType] = object
+  GPUValue*[T: object, TBuffer: static BufferType] = object
     data*: T
     buffer: Buffer
     offset: uint64
@@ -139,7 +140,7 @@
         let `valuename` {.inject.} = value
         body
         `bindingNumber`.inc
-    elif typeof(value) is object:
+    elif typeof(value) is GPUValue:
       block:
         const `fieldname` {.inject.} = theFieldname
         const `typename` {.inject.} = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
@@ -156,16 +157,18 @@
           let `valuename` {.inject.} = value
           body
           `bindingNumber`.inc
-      elif elementType(value) is object:
+      elif elementType(value) is GPUValue:
         block:
           const `fieldname` {.inject.} = theFieldname
           const `typename` {.inject.} = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
-          const `countname` {.inject.} = uint32(typeof(value).len)
+          const `countname` {.inject.} = len(value).uint32
           let `valuename` {.inject.} = value
           body
           `bindingNumber`.inc
       else:
         {.error: "Unsupported descriptor type: " & typetraits.name(typeof(value)).}
+    else:
+      {.error: "Unsupported descriptor type: " & typetraits.name(typeof(value)).}
 
 include ./rendering/vulkan_wrappers
 include ./rendering/renderpasses
--- a/semicongine/rendering/renderer.nim	Tue Jul 16 20:39:35 2024 +0700
+++ b/semicongine/rendering/renderer.nim	Wed Jul 17 00:30:49 2024 +0700
@@ -88,13 +88,26 @@
 ) =
 
   # santization checks
-  for name, value in descriptorSet.data.fieldPairs:
+  for theName, value in descriptorSet.data.fieldPairs:
     when typeof(value) is GPUValue:
       assert value.buffer.vk.Valid
     elif typeof(value) is Texture:
       assert value.vk.Valid
       assert value.imageview.Valid
       assert value.sampler.Valid
+    elif typeof(value) is array:
+      when elementType(value) is Texture:
+        for t in value:
+          assert t.vk.Valid
+          assert t.imageview.Valid
+          assert t.sampler.Valid
+      elif elementType(value) is GPUValue:
+        for t in value:
+          assert t.buffer.vk.Valid
+      else:
+        {.error: "Unsupported descriptor set field: '" & theName & "'".}
+    else:
+      {.error: "Unsupported descriptor set field: '" & theName & "'".}
 
   # allocate
   var layouts = newSeqWith(descriptorSet.vk.len, layout)
@@ -147,12 +160,11 @@
           pBufferInfo: nil,
         )
       elif typeof(fieldValue) is array:
-        discard
         when elementType(fieldValue) is Texture:
-          for textureIndex in 0 ..< descriptorCount:
+          for texture in fieldValue:
             imageWrites.add VkDescriptorImageInfo(
-              sampler: fieldValue[textureIndex].sampler,
-              imageView: fieldValue[textureIndex].imageView,
+              sampler: texture.sampler,
+              imageView: texture.imageView,
               imageLayout: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
             )
           descriptorSetWrites.add VkWriteDescriptorSet(
@@ -165,6 +177,23 @@
             pImageInfo: addr(imageWrites[^descriptorCount.int]),
             pBufferInfo: nil,
           )
+        elif elementType(fieldValue) is GPUValue:
+          for entry in fieldValue:
+            bufferWrites.add VkDescriptorBufferInfo(
+              buffer: entry.buffer.vk,
+              offset: entry.offset,
+              range: entry.size,
+            )
+          descriptorSetWrites.add VkWriteDescriptorSet(
+            sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+            dstSet: descriptorSet.vk[i],
+            dstBinding: descriptorBindingNumber,
+            dstArrayElement: 0,
+            descriptorType: descriptorType,
+            descriptorCount: descriptorCount,
+            pImageInfo: nil,
+            pBufferInfo: addr(bufferWrites[^descriptorCount.int]),
+          )
         else:
           {.error: "Unsupported descriptor type: " & typetraits.name(typeof(fieldValue)).}
       else:
@@ -260,35 +289,48 @@
   for name, fieldvalue in value.fieldPairs():
     when typeof(fieldvalue) is GPUData:
       UpdateGPUBuffer(fieldvalue)
+    when typeof(fieldvalue) is array:
+      when elementType(fieldvalue) is GPUData:
+        for entry in fieldvalue:
+          UpdateGPUBuffer(entry)
+
+proc AssignGPUData(renderdata: var RenderData, value: var GPUData) =
+  # find buffer that has space
+  var selectedBufferI = -1
+
+  for i in 0 ..< renderData.buffers[value.bufferType].len:
+    let buffer = renderData.buffers[value.bufferType][i]
+    if buffer.size - alignedTo(buffer.offsetNextFree, BUFFER_ALIGNMENT) >= value.size:
+      selectedBufferI = i
+
+  # otherwise create new buffer
+  if selectedBufferI < 0:
+    selectedBufferI = renderdata.buffers[value.bufferType].len
+    renderdata.buffers[value.bufferType].add renderdata.AllocateNewBuffer(
+      size = max(value.size, BUFFER_ALLOCATION_SIZE),
+      bufferType = value.bufferType,
+    )
+
+  # assigne value
+  let selectedBuffer = renderdata.buffers[value.bufferType][selectedBufferI]
+  renderdata.buffers[value.bufferType][selectedBufferI].offsetNextFree = alignedTo(
+    selectedBuffer.offsetNextFree,
+    BUFFER_ALIGNMENT
+  )
+  value.buffer = selectedBuffer
+  value.offset = renderdata.buffers[value.bufferType][selectedBufferI].offsetNextFree
+  renderdata.buffers[value.bufferType][selectedBufferI].offsetNextFree += value.size
 
 proc AssignBuffers*[T](renderdata: var RenderData, data: var T, uploadData = true) =
   for name, value in fieldPairs(data):
-    when typeof(value) is GPUData:
-
-      # find buffer that has space
-      var selectedBufferI = -1
-      for i in 0 ..< renderData.buffers[value.bufferType].len:
-        let buffer = renderData.buffers[value.bufferType][i]
-        if buffer.size - alignedTo(buffer.offsetNextFree, BUFFER_ALIGNMENT) >= value.size:
-          selectedBufferI = i
 
-      # otherwise create new buffer
-      if selectedBufferI < 0:
-        selectedBufferI = renderdata.buffers[value.bufferType].len
-        renderdata.buffers[value.bufferType].add renderdata.AllocateNewBuffer(
-          size = max(value.size, BUFFER_ALLOCATION_SIZE),
-          bufferType = value.bufferType,
-        )
+    when typeof(value) is GPUData:
+      AssignGPUData(renderdata, value)
+    elif typeof(value) is array:
+      when elementType(value) is GPUValue:
+        for v in value.mitems:
+          AssignGPUData(renderdata, v)
 
-      # assigne value
-      let selectedBuffer = renderdata.buffers[value.bufferType][selectedBufferI]
-      renderdata.buffers[value.bufferType][selectedBufferI].offsetNextFree = alignedTo(
-        selectedBuffer.offsetNextFree,
-        BUFFER_ALIGNMENT
-      )
-      value.buffer = selectedBuffer
-      value.offset = renderdata.buffers[value.bufferType][selectedBufferI].offsetNextFree
-      renderdata.buffers[value.bufferType][selectedBufferI].offsetNextFree += value.size
   if uploadData:
     UpdateAllGPUBuffers(data)
 
@@ -410,7 +452,7 @@
 
   texture.vk = svkCreate2DImage(texture.width, texture.height, format, usage)
   renderData.images.add texture.vk
-  texture.sampler = createSampler()
+  texture.sampler = createSampler(magFilter = texture.interpolation, minFilter = texture.interpolation)
   renderData.samplers.add texture.sampler
 
   let memoryRequirements = texture.vk.svkGetImageMemoryRequirements()
@@ -480,48 +522,6 @@
         let `fieldvalue` {.inject.} = value
         body
 
-proc AssertCompatible(TShader, TMesh, TInstance, TFirstDescriptorSet, TSecondDescriptorSet: typedesc) =
-  var descriptorSetCount = 0
-
-  for shaderAttributeName, shaderAttribute in default(TShader).fieldPairs:
-    var foundField = false
-
-    # Vertex input data
-    when hasCustomPragma(shaderAttribute, VertexAttribute):
-      assert typeof(shaderAttribute) is SupportedGPUType
-      for meshName, meshValue in default(TMesh).fieldPairs:
-        when meshName == shaderAttributeName:
-          assert meshValue is GPUArray, "Mesh attribute '" & meshName & "' must be of type 'GPUArray' but is of type " & typetraits.name(typeof(meshValue))
-          assert foundField == false, "Shader input '" & typetraits.name(TShader) & "." & shaderAttributeName & "' has been found more than once"
-          assert elementType(meshValue.data) is typeof(shaderAttribute), "Shader input " & typetraits.name(TShader) & "." & shaderAttributeName & " is of type '" & typetraits.name(typeof(shaderAttribute)) & "' but mesh attribute is of type '" & typetraits.name(elementType(meshValue.data)) & "'"
-          foundField = true
-      assert foundField, "Shader input '" & typetraits.name(TShader) & "." & shaderAttributeName & ": " & typetraits.name(typeof(shaderAttribute)) & "' not found in '" & typetraits.name(TMesh) & "'"
-
-    # Instance input data
-    elif hasCustomPragma(shaderAttribute, InstanceAttribute):
-      assert typeof(shaderAttribute) is SupportedGPUType
-      for instanceName, instanceValue in default(TInstance).fieldPairs:
-        when instanceName == shaderAttributeName:
-          assert instanceValue is GPUArray, "Instance attribute '" & instanceName & "' must be of type 'GPUArray' but is of type " & typetraits.name(typeof(instanceName))
-          assert foundField == false, "Shader input '" & typetraits.name(TShader) & "." & shaderAttributeName & "' has been found more than once"
-          assert elementType(instanceValue.data) is typeof(shaderAttribute), "Shader input " & typetraits.name(TShader) & "." & shaderAttributeName & " is of type '" & typetraits.name(typeof(shaderAttribute)) & "' but instance attribute is of type '" & typetraits.name(elementType(instanceValue.data)) & "'"
-          foundField = true
-      assert foundField, "Shader input '" & typetraits.name(TShader) & "." & shaderAttributeName & ": " & typetraits.name(typeof(shaderAttribute)) & "' not found in '" & typetraits.name(TInstance) & "'"
-
-    # descriptors
-    elif typeof(shaderAttribute) is DescriptorSet:
-      assert descriptorSetCount <= MAX_DESCRIPTORSETS.int, typetraits.name(TShader) & ": maximum " & $MAX_DESCRIPTORSETS & " allowed"
-      descriptorSetCount.inc
-
-
-      when shaderAttribute.sType == First:
-        assert shaderAttribute.sType == default(TFirstDescriptorSet).sType, "Shader has first descriptor set of type '" & $shaderAttribute.sType & "' but matching provided type is '" & $default(TFirstDescriptorSet).sType & "'"
-        assert typeof(shaderAttribute) is TFirstDescriptorSet, "Shader has first descriptor set type '" & typetraits.name(get(genericParams(typeof(shaderAttribute)), 0)) & "' but provided type is " & typetraits.name(TFirstDescriptorSet)
-      elif shaderAttribute.sType == Second:
-        assert shaderAttribute.sType == default(TSecondDescriptorSet).sType, "Shader has second descriptor set of type '" & $shaderAttribute.sType & "' but matching provided type is '" & $default(TSecondDescriptorSet).sType & "'"
-        assert typeof(shaderAttribute) is TSecondDescriptorSet, "Shader has seconddescriptor type '" & typetraits.name(get(genericParams(typeof(shaderAttribute)), 0)) & "' but provided type is " & typetraits.name(TSecondDescriptorSet)
-
-
 template WithBind*[A, B, C, D](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], DescriptorSet[B], DescriptorSet[C], DescriptorSet[D]), pipeline: Pipeline, currentFiF: int, body: untyped): untyped =
   block:
     var descriptorSets: seq[VkDescriptorSet]
@@ -555,17 +555,12 @@
     svkCmdBindDescriptorSets(commandBuffer, descriptorSets, pipeline.layout)
     body
 
-
-proc Render*[TFirstDescriptorSet, TSecondDescriptorSet: DescriptorSet, TShader, TMesh, TInstance](
+proc Render*[TShader, TMesh, TInstance](
   commandBuffer: VkCommandBuffer,
   pipeline: Pipeline[TShader],
-  firstSet: TFirstDescriptorSet,
-  secondSet: TSecondDescriptorSet,
   mesh: TMesh,
   instances: TInstance,
 ) =
-  when not defined(release):
-    static: AssertCompatible(TShader, TMesh, TInstance, TFirstDescriptorSet, TSecondDescriptorSet)
 
   var vertexBuffers: seq[VkBuffer]
   var vertexBuffersOffsets: seq[uint64]
@@ -643,44 +638,13 @@
     )
 
 type EMPTY = object
-type EMPTY_DESCRIPTORSET = DescriptorSet[EMPTY]
 
-proc Render*[TFirstDescriptorSet, TSecondDescriptorSet: DescriptorSet, TShader, TMesh](
-  commandBuffer: VkCommandBuffer,
-  pipeline: Pipeline[TShader],
-  firstSet: TFirstDescriptorSet,
-  secondSet: TSecondDescriptorSet,
-  mesh: TMesh,
-) =
-  Render(commandBuffer, pipeline, firstSet, secondSet, mesh, EMPTY())
-proc Render*[TFirstDescriptorSet: DescriptorSet, TMesh, TShader](
-  commandBuffer: VkCommandBuffer,
-  pipeline: Pipeline[TShader],
-  firstSet: TFirstDescriptorSet,
-  mesh: TMesh,
-) =
-  Render(commandBuffer, pipeline, firstSet, EMPTY_DESCRIPTORSET(), mesh, EMPTY())
 proc Render*[TShader, TMesh](
   commandBuffer: VkCommandBuffer,
   pipeline: Pipeline[TShader],
   mesh: TMesh,
 ) =
-  Render(commandBuffer, pipeline, EMPTY_DESCRIPTORSET(), EMPTY_DESCRIPTORSET(), mesh, EMPTY())
-proc Render*[TFirstDescriptorSet: DescriptorSet, TMesh, TShader, TInstance](
-  commandBuffer: VkCommandBuffer,
-  pipeline: Pipeline[TShader],
-  firstSet: TFirstDescriptorSet,
-  mesh: TMesh,
-  instances: TInstance,
-) =
-  Render(commandBuffer, pipeline, firstSet, EMPTY_DESCRIPTORSET(), mesh, instances)
-proc Render*[TShader, TMesh, TInstance](
-  commandBuffer: VkCommandBuffer,
-  pipeline: Pipeline[TShader],
-  mesh: TMesh,
-  instances: TInstance,
-) =
-  Render(commandBuffer, pipeline, EMPTY_DESCRIPTORSET(), EMPTY_DESCRIPTORSET(), mesh, instances)
+  Render(commandBuffer, pipeline, mesh, EMPTY())
 
 proc asGPUArray*[T](data: openArray[T], bufferType: static BufferType): auto =
   GPUArray[T, bufferType](data: @data)
--- a/semicongine/rendering/shaders.nim	Tue Jul 16 20:39:35 2024 +0700
+++ b/semicongine/rendering/shaders.nim	Wed Jul 17 00:30:49 2024 +0700
@@ -166,6 +166,7 @@
       for descriptor in value.fields:
 
         var descriptorBinding = 0
+
         for descriptorName, descriptorValue in fieldPairs(descriptor):
 
           when typeof(descriptorValue) is Texture:
@@ -173,25 +174,40 @@
             descriptorBinding.inc
 
           elif typeof(descriptorValue) is GPUValue:
+
             uniforms.add "layout(set=" & $descriptorSetIndex & ", binding = " & $descriptorBinding & ") uniform T" & descriptorName & " {"
             when typeof(descriptorValue.data) is object:
+
               for blockFieldName, blockFieldValue in descriptorValue.data.fieldPairs():
                 assert typeof(blockFieldValue) is SupportedGPUType, "uniform block field '" & blockFieldName & "' is not a SupportedGPUType"
                 uniforms.add "  " & GlslType(blockFieldValue) & " " & blockFieldName & ";"
               uniforms.add "} " & descriptorName & ";"
-            elif typeof(descriptorValue.data) is array:
-              for blockFieldName, blockFieldValue in default(elementType(descriptorValue.data)).fieldPairs():
-                assert typeof(blockFieldValue) is SupportedGPUType, "uniform block field '" & blockFieldName & "' is not a SupportedGPUType"
-                uniforms.add "  " & GlslType(blockFieldValue) & " " & blockFieldName & ";"
-              uniforms.add "} " & descriptorName & "[" & $descriptorValue.data.len & "];"
+
+            else:
+              {.error: "Unsupported shader descriptor field " & descriptorName & " (must be object)".}
             descriptorBinding.inc
+
           elif typeof(descriptorValue) is array:
+
             when elementType(descriptorValue) is Texture:
+
               let arrayDecl = "[" & $typeof(descriptorValue).len & "]"
               samplers.add "layout(set=" & $descriptorSetIndex & ", binding = " & $descriptorBinding & ") uniform " & GlslType(default(elementType(descriptorValue))) & " " & descriptorName & "" & arrayDecl & ";"
               descriptorBinding.inc
+
+            elif elementType(descriptorValue) is GPUValue:
+
+              uniforms.add "layout(set=" & $descriptorSetIndex & ", binding = " & $descriptorBinding & ") uniform T" & descriptorName & " {"
+
+              for blockFieldName, blockFieldValue in default(elementType(descriptorValue)).data.fieldPairs():
+                assert typeof(blockFieldValue) is SupportedGPUType, "uniform block field '" & blockFieldName & "' is not a SupportedGPUType"
+                uniforms.add "  " & GlslType(blockFieldValue) & " " & blockFieldName & ";"
+              uniforms.add "} " & descriptorName & "[" & $descriptorValue.len & "];"
+              descriptorBinding.inc
+
             else:
               {.error: "Unsupported shader descriptor field " & descriptorName.}
+
         descriptorSetIndex.inc
     elif fieldname in ["vertexCode", "fragmentCode"]:
       discard
@@ -304,9 +320,9 @@
   var setNumber: int
   for _, value in fieldPairs(default(TShader)):
     when hasCustomPragma(value, DescriptorSets):
-      for descriptor in value.fields:
+      for descriptorSet in value.fields:
         var layoutbindings: seq[VkDescriptorSetLayoutBinding]
-        ForDescriptorFields(descriptor, fieldName, fieldValue, descriptorType, descriptorCount, descriptorBindingNumber):
+        ForDescriptorFields(descriptorSet, fieldName, fieldValue, descriptorType, descriptorCount, descriptorBindingNumber):
           layoutbindings.add VkDescriptorSetLayoutBinding(
             binding: descriptorBindingNumber,
             descriptorType: descriptorType,
Binary file tests/test_rendering has changed
--- a/tests/test_rendering.nim	Tue Jul 16 20:39:35 2024 +0700
+++ b/tests/test_rendering.nim	Wed Jul 17 00:30:49 2024 +0700
@@ -119,31 +119,126 @@
   DestroyPipeline(pipeline)
   DestroyRenderData(renderdata)
 
-proc test_03_global_descriptorset(nFrames: int) =
+proc test_03_simple_descriptorset(nFrames: int) =
+  var renderdata = InitRenderData()
+
+  type
+    Material = object
+      baseColor: Vec3f
+
+    Uniforms = object
+      material: GPUValue[Material, UniformBuffer]
+      texture1: Texture[TVec3[uint8]]
+
+    QuadShader = object
+      position {.VertexAttribute.}: Vec3f
+      fragmentColor {.Pass.}: Vec3f
+      uv {.Pass.}: Vec2f
+      outColor {.ShaderOutput.}: Vec4f
+      descriptorSets {.DescriptorSets.}: (Uniforms, )
+      # code
+      vertexCode: string = """void main() {
+      fragmentColor = material.baseColor;
+      gl_Position = vec4(position, 1);
+      gl_Position.x += ((material.baseColor.b - 0.5) * 2) - 0.5;
+      uv = position.xy + 0.5;
+      }"""
+      fragmentCode: string = """void main() {
+      outColor = vec4(fragmentColor, 1) * texture(texture1, uv);}"""
+    QuadMesh = object
+      position: GPUArray[Vec3f, VertexBuffer]
+      indices: GPUArray[uint16, IndexBuffer]
+
+  let R = TVec3[uint8]([255'u8, 0'u8, 0'u8])
+  let G = TVec3[uint8]([0'u8, 255'u8, 0'u8])
+  let B = TVec3[uint8]([0'u8, 0'u8, 255'u8])
+  let W = TVec3[uint8]([255'u8, 255'u8, 255'u8])
+  var
+    quad = QuadMesh(
+      position: asGPUArray([NewVec3f(-0.5, -0.5), NewVec3f(-0.5, 0.5), NewVec3f(0.5, 0.5), NewVec3f(0.5, -0.5)], VertexBuffer),
+      indices: asGPUArray([0'u16, 1'u16, 2'u16, 2'u16, 3'u16, 0'u16], IndexBuffer),
+    )
+    uniforms1 = asDescriptorSet(
+      Uniforms(
+        material: asGPUValue(Material(baseColor: NewVec3f(1, 1, 1)), UniformBuffer),
+        texture1: Texture[TVec3[uint8]](width: 3, height: 3, data: @[R, G, B, G, B, R, B, R, G], interpolation: VK_FILTER_NEAREST),
+      )
+    )
+    uniforms2 = asDescriptorSet(
+      Uniforms(
+        material: asGPUValue(Material(baseColor: NewVec3f(0.5, 0.5, 0.5)), UniformBuffer),
+        texture1: Texture[TVec3[uint8]](width: 2, height: 2, data: @[R, G, B, W]),
+      )
+    )
+
+  AssignBuffers(renderdata, quad)
+  AssignBuffers(renderdata, uniforms1)
+  AssignBuffers(renderdata, uniforms2)
+  UploadTextures(renderdata, uniforms1)
+  UploadTextures(renderdata, uniforms2)
+  renderdata.FlushAllMemory()
+
+  var pipeline = CreatePipeline[QuadShader](renderPass = mainRenderpass, samples = swapchain.samples)
+
+  InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], uniforms1)
+  InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], uniforms2)
+
+  var c = 0
+  while UpdateInputs() and c < nFrames:
+    WithNextFrame(swapchain, framebuffer, commandbuffer):
+      WithRenderPass(mainRenderpass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+        WithPipeline(commandbuffer, pipeline):
+          WithBind(commandbuffer, (uniforms1, ), pipeline, swapchain.currentFiF):
+            Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
+          WithBind(commandbuffer, (uniforms2, ), pipeline, swapchain.currentFiF):
+            Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
+    inc c
+
+  # cleanup
+  checkVkResult vkDeviceWaitIdle(vulkan.device)
+  DestroyPipeline(pipeline)
+  DestroyRenderData(renderdata)
+
+proc test_04_multiple_descriptorsets(nFrames: int) =
   var renderdata = InitRenderData()
 
   type
     RenderSettings = object
       brigthness: float32
+    Material = object
+      baseColor: Vec3f
     ObjectSettings = object
-      baseColor: Vec3f
+      scale: float32
+      materialIndex: uint32
+    Constants = object
+      offset: Vec2f
+
+    ConstSet = object
+      constants: GPUValue[Constants, UniformBuffer]
     MainSet = object
       renderSettings: GPUValue[RenderSettings, UniformBufferMapped]
-      materialSettings: GPUValue[ObjectSettings, UniformBuffer]
+      material: array[2, GPUValue[Material, UniformBuffer]]
+      texture1: array[2, Texture[TVec1[uint8]]]
     OtherSet = object
-    # TODO
+      objectSettings: GPUValue[ObjectSettings, UniformBufferMapped]
 
     QuadShader = object
       position {.VertexAttribute.}: Vec3f
       fragmentColor {.Pass.}: Vec3f
+      uv {.Pass.}: Vec2f
       outColor {.ShaderOutput.}: Vec4f
-      descriptorSets {.DescriptorSets.}: (MainSet, OtherSet)
+      descriptorSets {.DescriptorSets.}: (ConstSet, MainSet, OtherSet)
       # code
       vertexCode: string = """void main() {
-      fragmentColor = materialSettings.baseColor * renderSettings.brigthness;
-      gl_Position = vec4(position, 1);}"""
+      fragmentColor = material[objectSettings.materialIndex].baseColor * renderSettings.brigthness;
+      gl_Position = vec4(position * objectSettings.scale, 1);
+      gl_Position.xy += constants.offset.xy;
+      gl_Position.x += material[objectSettings.materialIndex].baseColor.b - 0.5;
+      uv = position.xy + 0.5;
+      }"""
       fragmentCode: string = """void main() {
-      outColor = vec4(fragmentColor, 1);}"""
+      outColor = vec4(fragmentColor * texture(texture1[objectSettings.materialIndex], uv).rrr, 1);
+      }"""
     QuadMesh = object
       position: GPUArray[Vec3f, VertexBuffer]
       indices: GPUArray[uint16, IndexBuffer]
@@ -152,35 +247,65 @@
     position: asGPUArray([NewVec3f(-0.5, -0.5), NewVec3f(-0.5, 0.5), NewVec3f(0.5, 0.5), NewVec3f(0.5, -0.5)], VertexBuffer),
     indices: asGPUArray([0'u16, 1'u16, 2'u16, 2'u16, 3'u16, 0'u16], IndexBuffer),
   )
-  var mainSet = asDescriptorSet(
+  var constset = asDescriptorSet(
+    ConstSet(
+      constants: asGPUValue(Constants(offset: NewVec2f(-0.3, 0.2)), UniformBuffer),
+    )
+  )
+  let G = TVec1[uint8]([50'u8])
+  let W = TVec1[uint8]([255'u8])
+  var mainset = asDescriptorSet(
     MainSet(
       renderSettings: asGPUValue(RenderSettings(brigthness: 0), UniformBufferMapped),
-      materialSettings: asGPUValue(ObjectSettings(baseColor: NewVec3f(1, 1, 0)), UniformBuffer),
+      material: [
+        asGPUValue(Material(baseColor: NewVec3f(1, 1, 0)), UniformBuffer),
+        asGPUValue(Material(baseColor: NewVec3f(1, 0, 1)), UniformBuffer),
+    ],
+    texture1: [
+      Texture[TVec1[uint8]](width: 2, height: 2, data: @[W, G, G, W], interpolation: VK_FILTER_NEAREST),
+      Texture[TVec1[uint8]](width: 3, height: 3, data: @[W, G, W, G, W, G, W, G, W], interpolation: VK_FILTER_NEAREST),
+    ],
+  ),
+  )
+  var otherset1 = asDescriptorSet(
+    OtherSet(
+      objectSettings: asGPUValue(ObjectSettings(scale: 1.0, materialIndex: 0), UniformBufferMapped),
     )
   )
-  var settings = asDescriptorSet(
+  var otherset2 = asDescriptorSet(
     OtherSet(
-    # TODO
+      objectSettings: asGPUValue(ObjectSettings(scale: 1.0, materialIndex: 1), UniformBufferMapped),
     )
   )
 
   AssignBuffers(renderdata, quad)
-  AssignBuffers(renderdata, mainSet)
+  AssignBuffers(renderdata, constset)
+  AssignBuffers(renderdata, mainset)
+  AssignBuffers(renderdata, otherset1)
+  AssignBuffers(renderdata, otherset2)
+  UploadTextures(renderdata, mainset)
   renderdata.FlushAllMemory()
 
   var pipeline = CreatePipeline[QuadShader](renderPass = mainRenderpass, samples = swapchain.samples)
 
-  InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], mainSet)
+  InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], constset)
+  InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[1], mainset)
+  InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[2], otherset1)
+  InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[2], otherset2)
 
   var c = 0
   while UpdateInputs() and c < nFrames:
     WithNextFrame(swapchain, framebuffer, commandbuffer):
-      WithBind(commandbuffer, (mainSet, ), pipeline, swapchain.currentFiF):
-        WithRenderPass(mainRenderpass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
-          WithPipeline(commandbuffer, pipeline):
-            Render(commandbuffer = commandbuffer, pipeline = pipeline, firstSet = mainSet, mesh = quad)
-    mainSet.data.renderSettings.data.brigthness = (c.float32 / nFrames.float32)
-    UpdateGPUBuffer(mainSet.data.renderSettings)
+      WithRenderPass(mainRenderpass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+        WithPipeline(commandbuffer, pipeline):
+          WithBind(commandbuffer, (constset, mainset, otherset1), pipeline, swapchain.currentFiF):
+            Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
+          WithBind(commandbuffer, (constset, mainset, otherset2), pipeline, swapchain.currentFiF):
+            Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
+    mainset.data.renderSettings.data.brigthness = (c.float32 / nFrames.float32)
+    otherset1.data.objectSettings.data.scale = 0.5 + (c.float32 / nFrames.float32)
+    UpdateGPUBuffer(mainset.data.renderSettings)
+    UpdateGPUBuffer(otherset1.data.objectSettings)
     renderdata.FlushAllMemory()
     inc c
 
@@ -204,8 +329,10 @@
     # tests instanced triangles and quads, mixing meshes and instances
     # test_02_triangle_quad_instanced(nFrames)
 
+    # test_03_simple_descriptorset(nFrames)
+
     # tests
-    test_03_global_descriptorset(nFrames)
+    test_04_multiple_descriptorsets(nFrames)
 
     checkVkResult vkDeviceWaitIdle(vulkan.device)
     vkDestroyRenderPass(vulkan.device, mainRenderpass, nil)