changeset 1239:69489a678141

add: better syncing, better swapchain access, correct font offset, two font-rendering tests
author sam <sam@basx.dev>
date Mon, 22 Jul 2024 00:46:10 +0700
parents 03634915bbdb
children 42eeb59f3a43
files semiconginev2/rendering.nim semiconginev2/rendering/renderer.nim semiconginev2/rendering/swapchain.nim semiconginev2/rendering/vulkan_wrappers.nim semiconginev2/text/textbox.nim tests/test_rendering.nim tests/test_text.nim
diffstat 7 files changed, 247 insertions(+), 146 deletions(-) [+]
line wrap: on
line diff
--- a/semiconginev2/rendering.nim	Sun Jul 21 11:31:11 2024 +0700
+++ b/semiconginev2/rendering.nim	Mon Jul 22 00:46:10 2024 +0700
@@ -42,6 +42,8 @@
     graphicsQueueFamily*: uint32
     graphicsQueue*: VkQueue
     debugMessenger: VkDebugUtilsMessengerEXT
+    # populated through the InitSwapchain proc
+    swapchain*: Swapchain
     # unclear as of yet
     anisotropy*: float32 = 0 # needs to be enable during device creation
   Renderpass* = ref object
@@ -80,8 +82,6 @@
 var vulkan*: VulkanGlobals
 var fullscreen: bool
 
-func currentFiF*(swapchain: Swapchain): int = swapchain.currentFiF
-
 type
   # type aliases
   SupportedGPUType = 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]
@@ -320,7 +320,18 @@
   )
   vulkan.graphicsQueue = svkGetDeviceQueue(vulkan.device, vulkan.graphicsQueueFamily, VK_QUEUE_GRAPHICS_BIT)
 
+proc ClearSwapchain*() =
+  assert vulkan.swapchain != nil, "Swapchain has not been initialized yet"
+  DestroySwapchain(vulkan.swapchain)
+  vulkan.swapchain = nil
+
+proc SetupSwapchain*(renderPass: RenderPass, vSync: bool = false) =
+  assert vulkan.swapchain == nil, "Swapchain has already been initialized yet"
+  vulkan.swapchain = InitSwapchain(renderPass, vSync = vSync)
+
 proc DestroyVulkan*() =
+  if vulkan.swapchain != nil:
+    DestroySwapchain(vulkan.swapchain)
   vkDestroyDevice(vulkan.device, nil)
   vkDestroySurfaceKHR(vulkan.instance, vulkan.surface, nil)
   vkDestroyDebugUtilsMessengerEXT(vulkan.instance, vulkan.debugMessenger, nil)
@@ -334,7 +345,13 @@
     fullscreen = enable
     vulkan.window.Fullscreen(fullscreen)
 
-func GetAspectRatio*(swapchain: Swapchain): float32 = swapchain.width.float32 / swapchain.height.float32
+proc GetAspectRatio*(): float32 =
+  assert vulkan.swapchain != nil, "Swapchain has not been initialized yet"
+  vulkan.swapchain.width.float32 / vulkan.swapchain.height.float32
+
+proc currentFiF*(): int =
+  assert vulkan.swapchain != nil, "Swapchain has not been initialized yet"
+  vulkan.swapchain.currentFiF
 
 proc MaxFramebufferSampleCount*(maxSamples = VK_SAMPLE_COUNT_8_BIT): VkSampleCountFlagBits =
   let limits = svkGetPhysicalDeviceProperties().limits
--- a/semiconginev2/rendering/renderer.nim	Sun Jul 21 11:31:11 2024 +0700
+++ b/semiconginev2/rendering/renderer.nim	Mon Jul 22 00:46:10 2024 +0700
@@ -4,11 +4,11 @@
 func usage(bType: BufferType): seq[VkBufferUsageFlagBits] =
   case bType:
     of VertexBuffer: @[VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_BUFFER_USAGE_TRANSFER_DST_BIT]
-    of VertexBufferMapped: @[VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_BUFFER_USAGE_TRANSFER_DST_BIT]
+    of VertexBufferMapped: @[VK_BUFFER_USAGE_VERTEX_BUFFER_BIT]
     of IndexBuffer: @[VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_BUFFER_USAGE_TRANSFER_DST_BIT]
-    of IndexBufferMapped: @[VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_BUFFER_USAGE_TRANSFER_DST_BIT]
+    of IndexBufferMapped: @[VK_BUFFER_USAGE_INDEX_BUFFER_BIT]
     of UniformBuffer: @[VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_BUFFER_USAGE_TRANSFER_DST_BIT]
-    of UniformBufferMapped: @[VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_BUFFER_USAGE_TRANSFER_DST_BIT]
+    of UniformBufferMapped: @[VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT]
 
 proc GetVkFormat(grayscale: bool, usage: openArray[VkImageUsageFlagBits]): VkFormat =
   let formats = if grayscale: [VK_FORMAT_R8_SRGB, VK_FORMAT_R8_UNORM]
@@ -279,9 +279,10 @@
   result.rawPointer = selectedBlock.rawPointer.pointerAddOffset(selectedBlock.offsetNextFree)
   renderData.memory[memoryType][selectedBlockI].offsetNextFree += memoryRequirements.size
 
-proc UpdateGPUBuffer*(gpuData: GPUData, flush = false) =
+proc UpdateGPUBuffer*(gpuData: GPUData, flush = false, allFrames = false) =
   if gpuData.size == 0:
     return
+
   when NeedsMapping(gpuData):
     copyMem(pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset), gpuData.rawPointer, gpuData.size)
     if flush:
@@ -290,47 +291,57 @@
     WithStagingBuffer((gpuData.buffer.vk, gpuData.offset), gpuData.size, stagingPtr):
       copyMem(stagingPtr, gpuData.rawPointer, gpuData.size)
 
-proc UpdateAllGPUBuffers*[T](value: T, flush = false) =
+proc UpdateAllGPUBuffers*[T](value: T, flush = false, allFrames = false) =
   for name, fieldvalue in value.fieldPairs():
     when typeof(fieldvalue) is GPUData:
-      UpdateGPUBuffer(fieldvalue, flush = flush)
+      UpdateGPUBuffer(fieldvalue, flush = flush, allFrames = allFrames)
     when typeof(fieldvalue) is array:
       when elementType(fieldvalue) is GPUData:
         for entry in fieldvalue:
-          UpdateGPUBuffer(entry, flush = flush)
+          UpdateGPUBuffer(entry, flush = flush, allFrames = allFrames)
 
-proc AssignGPUData(renderdata: var RenderData, value: var GPUData) =
+proc AllocateGPUData(
+  renderdata: var RenderData,
+  bufferType: BufferType,
+  size: uint64,
+  needsFrameInFlight = -1
+): (Buffer, uint64) =
+
   # 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
+  for i in 0 ..< renderData.buffers[bufferType].len:
+    let buffer = renderData.buffers[bufferType][i]
+    if needsFrameInFlight == -1 or buffer.useForFrameInFlight == needsFrameInFlight:
+      if buffer.size - alignedTo(buffer.offsetNextFree, BUFFER_ALIGNMENT) >= 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,
+    selectedBufferI = renderdata.buffers[bufferType].len
+    renderdata.buffers[bufferType].add renderdata.AllocateNewBuffer(
+      size = max(size, BUFFER_ALLOCATION_SIZE),
+      bufferType = bufferType,
     )
+    if needsFrameInFlight >= 0:
+      renderdata.buffers[bufferType][selectedBufferI].useForFrameInFlight = needsFrameInFlight
 
   # assigne value
-  let selectedBuffer = renderdata.buffers[value.bufferType][selectedBufferI]
-  renderdata.buffers[value.bufferType][selectedBufferI].offsetNextFree = alignedTo(
+  let selectedBuffer = renderdata.buffers[bufferType][selectedBufferI]
+  renderdata.buffers[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
+
+  result[0] = selectedBuffer
+  result[1] = renderdata.buffers[bufferType][selectedBufferI].offsetNextFree
+  renderdata.buffers[bufferType][selectedBufferI].offsetNextFree += size
 
 proc AssignBuffers*[T](renderdata: var RenderData, data: var T, uploadData = true) =
   for name, value in fieldPairs(data):
 
     when typeof(value) is GPUData:
-      AssignGPUData(renderdata, value)
+      (value.buffer, value.offset) = AllocateGPUData(renderdata, value.bufferType, value.size)
 
     elif typeof(value) is DescriptorSet:
       AssignBuffers(renderdata, value.data, uploadData = uploadData)
@@ -338,10 +349,10 @@
     elif typeof(value) is array:
       when elementType(value) is GPUValue:
         for v in value.mitems:
-          AssignGPUData(renderdata, v)
+          (v.buffer, v.offset) = AllocateGPUData(renderdata, v.bufferType, v.size)
 
   if uploadData:
-    UpdateAllGPUBuffers(data)
+    UpdateAllGPUBuffers(data, flush = true, allFrames = true)
 
 proc AssignBuffers*(renderdata: var RenderData, descriptorSet: var DescriptorSet, uploadData = true) =
   AssignBuffers(renderdata, descriptorSet.data, uploadData = uploadData)
@@ -534,36 +545,36 @@
         let `fieldvalue` {.inject.} = value
         body
 
-template WithBind*[A, B, C, D](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], DescriptorSet[B], DescriptorSet[C], DescriptorSet[D]), pipeline: Pipeline, currentFiF: int, body: untyped): untyped =
+template WithBind*[A, B, C, D](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], DescriptorSet[B], DescriptorSet[C], DescriptorSet[D]), pipeline: Pipeline, body: untyped): untyped =
   block:
     var descriptorSets: seq[VkDescriptorSet]
     for dSet in sets.fields:
-      assert dSet.vk[currentFiF].Valid, "DescriptorSet not initialized, maybe forgot to call InitDescriptorSet"
-      descriptorSets.add dSet.vk[currentFiF]
+      assert dSet.vk[currentFiF()].Valid, "DescriptorSet not initialized, maybe forgot to call InitDescriptorSet"
+      descriptorSets.add dSet.vk[currentFiF()]
     svkCmdBindDescriptorSets(commandBuffer, descriptorSets, pipeline.layout)
     body
-template WithBind*[A, B, C](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], DescriptorSet[B], DescriptorSet[C]), pipeline: Pipeline, currentFiF: int, body: untyped): untyped =
+template WithBind*[A, B, C](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], DescriptorSet[B], DescriptorSet[C]), pipeline: Pipeline, body: untyped): untyped =
   block:
     var descriptorSets: seq[VkDescriptorSet]
     for dSet in sets.fields:
-      assert dSet.vk[currentFiF].Valid, "DescriptorSet not initialized, maybe forgot to call InitDescriptorSet"
-      descriptorSets.add dSet.vk[currentFiF]
+      assert dSet.vk[currentFiF()].Valid, "DescriptorSet not initialized, maybe forgot to call InitDescriptorSet"
+      descriptorSets.add dSet.vk[currentFiF()]
     svkCmdBindDescriptorSets(commandBuffer, descriptorSets, pipeline.layout)
     body
-template WithBind*[A, B](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], DescriptorSet[B]), pipeline: Pipeline, currentFiF: int, body: untyped): untyped =
+template WithBind*[A, B](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], DescriptorSet[B]), pipeline: Pipeline, body: untyped): untyped =
   block:
     var descriptorSets: seq[VkDescriptorSet]
     for dSet in sets.fields:
-      assert dSet.vk[currentFiF].Valid, "DescriptorSet not initialized, maybe forgot to call InitDescriptorSet"
-      descriptorSets.add dSet.vk[currentFiF]
+      assert dSet.vk[currentFiF()].Valid, "DescriptorSet not initialized, maybe forgot to call InitDescriptorSet"
+      descriptorSets.add dSet.vk[currentFiF()]
     svkCmdBindDescriptorSets(commandBuffer, descriptorSets, pipeline.layout)
     body
-template WithBind*[A](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], ), pipeline: Pipeline, currentFiF: int, body: untyped): untyped =
+template WithBind*[A](commandBuffer: VkCommandBuffer, sets: (DescriptorSet[A], ), pipeline: Pipeline, body: untyped): untyped =
   block:
     var descriptorSets: seq[VkDescriptorSet]
     for dSet in sets.fields:
-      assert dSet.vk[currentFiF].Valid, "DescriptorSet not initialized, maybe forgot to call InitDescriptorSet"
-      descriptorSets.add dSet.vk[currentFiF]
+      assert dSet.vk[currentFiF()].Valid, "DescriptorSet not initialized, maybe forgot to call InitDescriptorSet"
+      descriptorSets.add dSet.vk[currentFiF()]
     svkCmdBindDescriptorSets(commandBuffer, descriptorSets, pipeline.layout)
     body
 
--- a/semiconginev2/rendering/swapchain.nim	Sun Jul 21 11:31:11 2024 +0700
+++ b/semiconginev2/rendering/swapchain.nim	Mon Jul 22 00:46:10 2024 +0700
@@ -1,10 +1,10 @@
 const N_FRAMEBUFFERS = 3'u32
 
-proc InitSwapchain*(
+proc InitSwapchain(
   renderPass: RenderPass,
   vSync: bool = false,
   oldSwapchain: Swapchain = nil,
-): Option[Swapchain] =
+): Swapchain =
   assert vulkan.instance.Valid, "Vulkan not initialized"
 
   var capabilities: VkSurfaceCapabilitiesKHR
@@ -14,7 +14,7 @@
     height = capabilities.currentExtent.height
 
   if width == 0 or height == 0:
-    return none(Swapchain)
+    return nil
 
   # following "count" is established according to vulkan specs
   var minFramebufferCount = N_FRAMEBUFFERS
@@ -49,7 +49,7 @@
   )
 
   if vkCreateSwapchainKHR(vulkan.device, addr(swapchainCreateInfo), nil, addr(swapchain.vk)) != VK_SUCCESS:
-    return none(Swapchain)
+    return nil
 
   if swapchain.oldSwapchain != nil:
     swapchain.oldSwapchainCounter = INFLIGHTFRAMES.int * 2
@@ -150,9 +150,11 @@
   )
   checkVkResult vkAllocateCommandBuffers(vulkan.device, addr(allocInfo), swapchain.commandBuffers.ToCPointer)
 
-  return some(swapchain)
+  return swapchain
 
 proc DestroySwapchain*(swapchain: Swapchain) =
+  if swapchain.oldSwapchain != nil:
+    DestroySwapchain(swapchain.oldSwapchain)
 
   if swapchain.msaaImage.Valid:
     vkDestroyImageView(vulkan.device, swapchain.msaaImageView, nil)
@@ -245,19 +247,20 @@
   swapchain.currentFiF = (uint32(swapchain.currentFiF) + 1) mod INFLIGHTFRAMES
   return true
 
-proc Recreate(swapchain: Swapchain): Option[Swapchain] =
+proc Recreate(swapchain: Swapchain): Swapchain =
   InitSwapchain(
     renderPass = swapchain.renderPass,
     vSync = swapchain.vSync,
     oldSwapchain = swapchain,
   )
 
-template WithNextFrame*(theSwapchain: var Swapchain, framebufferName, commandBufferName, body: untyped): untyped =
-  var maybeFramebuffer = TryAcquireNextImage(theSwapchain)
+template WithNextFrame*(framebufferName, commandBufferName, body: untyped): untyped =
+  assert vulkan.swapchain != nil, "Swapchain has not been initialized yet"
+  var maybeFramebuffer = TryAcquireNextImage(vulkan.swapchain)
   if maybeFramebuffer.isSome:
     block:
       let `framebufferName` {.inject.} = maybeFramebuffer.get
-      let `commandBufferName` {.inject.} = theSwapchain.commandBuffers[theSwapchain.currentFiF]
+      let `commandBufferName` {.inject.} = vulkan.swapchain.commandBuffers[vulkan.swapchain.currentFiF]
       let beginInfo = VkCommandBufferBeginInfo(
         sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
         flags: VkCommandBufferUsageFlags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT),
@@ -268,9 +271,8 @@
       body
 
       checkVkResult vkEndCommandBuffer(`commandBufferName`)
-      discard Swap(swapchain = theSwapchain, commandBuffer = `commandBufferName`)
+      discard Swap(swapchain = vulkan.swapchain, commandBuffer = `commandBufferName`)
   else:
-    let maybeNewSwapchain = Recreate(theSwapchain)
-    if maybeNewSwapchain.isSome:
-      theSwapchain = maybeNewSwapchain.get
-
+    let newSwapchain = Recreate(vulkan.swapchain)
+    if newSwapchain != nil:
+      vulkan.swapchain = newSwapchain
--- a/semiconginev2/rendering/vulkan_wrappers.nim	Sun Jul 21 11:31:11 2024 +0700
+++ b/semiconginev2/rendering/vulkan_wrappers.nim	Mon Jul 22 00:46:10 2024 +0700
@@ -347,6 +347,22 @@
 
   WithSingleUseCommandBuffer(commandBuffer):
     when T is (VkBuffer, uint64):
+      # first make sure memory has been made available with a memory barrier
+      # we are just waiting for the vertex input stage, but I think that is fine for most buffer copies (for now at least)
+      let memoryBarrier = VkMemoryBarrier(sType: VK_STRUCTURE_TYPE_MEMORY_BARRIER)
+      vkCmdPipelineBarrier(
+        commandBuffer = commandBuffer,
+        srcStageMask = toBits [VK_PIPELINE_STAGE_VERTEX_INPUT_BIT],
+        dstStageMask = toBits [VK_PIPELINE_STAGE_TRANSFER_BIT],
+        dependencyFlags = VkDependencyFlags(0),
+        memoryBarrierCount = 1,
+        pMemoryBarriers = addr(memoryBarrier),
+        bufferMemoryBarrierCount = 0,
+        pBufferMemoryBarriers = nil,
+        imageMemoryBarrierCount = 0,
+        pImageMemoryBarriers = nil,
+      )
+      # now copy stuff
       let copyRegion = VkBufferCopy(
         size: bufferSize,
         dstOffset: target[1],
--- a/semiconginev2/text/textbox.nim	Sun Jul 21 11:31:11 2024 +0700
+++ b/semiconginev2/text/textbox.nim	Mon Jul 22 00:46:10 2024 +0700
@@ -23,7 +23,7 @@
   "\"" & $textbox.text[0 ..< min(textbox.text.len, 16)] & "\""
 
 proc RefreshShaderdata(textbox: Textbox) =
-  textbox.shaderdata.data.textbox.UpdateGPUBuffer()
+  textbox.shaderdata.data.textbox.UpdateGPUBuffer(flush = true)
 
 proc RefreshGeometry(textbox: var Textbox) =
   # pre-calculate text-width
@@ -45,8 +45,9 @@
 
   let anchorY = (case textbox.verticalAlignment
     of Top: 0'f32
-    of Center: height / 2
-    of Bottom: height) - textbox.font.capHeight
+    of Center: -height / 2
+    of Bottom: -height
+  ) - textbox.font.capHeight
 
   var
     offsetX = 0'f32
@@ -61,7 +62,7 @@
     if i < textbox.processedText.len:
       if textbox.processedText[i] == Rune('\n'):
         offsetX = 0
-        offsetY += textbox.font.lineAdvance
+        offsetY -= textbox.font.lineAdvance
         textbox.position.data[vertexOffset + 0] = NewVec3f()
         textbox.position.data[vertexOffset + 1] = NewVec3f()
         textbox.position.data[vertexOffset + 2] = NewVec3f()
@@ -76,13 +77,13 @@
           glyph = textbox.font.glyphs[textbox.processedText[i]]
           left = offsetX + glyph.leftOffset
           right = offsetX + glyph.leftOffset + glyph.dimension.x
-          top = offsetY + glyph.topOffset
-          bottom = offsetY + glyph.topOffset + glyph.dimension.y
+          top = offsetY - glyph.topOffset
+          bottom = offsetY - glyph.topOffset - glyph.dimension.y
 
-        textbox.position.data[vertexOffset + 1] = NewVec3f(left - anchorX, bottom - anchorY)
-        textbox.position.data[vertexOffset + 0] = NewVec3f(left - anchorX, top - anchorY)
-        textbox.position.data[vertexOffset + 3] = NewVec3f(right - anchorX, top - anchorY)
-        textbox.position.data[vertexOffset + 2] = NewVec3f(right - anchorX, bottom - anchorY)
+        textbox.position.data[vertexOffset + 0] = NewVec3f(left - anchorX, bottom - anchorY)
+        textbox.position.data[vertexOffset + 1] = NewVec3f(left - anchorX, top - anchorY)
+        textbox.position.data[vertexOffset + 2] = NewVec3f(right - anchorX, top - anchorY)
+        textbox.position.data[vertexOffset + 3] = NewVec3f(right - anchorX, bottom - anchorY)
 
         textbox.uv.data[vertexOffset + 0] = glyph.uvs[0]
         textbox.uv.data[vertexOffset + 1] = glyph.uvs[1]
@@ -97,8 +98,8 @@
       textbox.position.data[vertexOffset + 1] = NewVec3f()
       textbox.position.data[vertexOffset + 2] = NewVec3f()
       textbox.position.data[vertexOffset + 3] = NewVec3f()
-  UpdateGPUBuffer(textbox.position, flush = true)
-  UpdateGPUBuffer(textbox.uv, flush = true)
+  UpdateGPUBuffer(textbox.position)
+  UpdateGPUBuffer(textbox.uv)
   textbox.lastRenderedText = textbox.processedText
 
 func text*(textbox: Textbox): seq[Rune] =
@@ -137,14 +138,6 @@
     textbox.dirtyShaderdata = true
     textbox.shaderdata.data.textbox.data.scale = value
 
-proc AspectRatio*(textbox: Textbox): float32 =
-  textbox.shaderdata.data.textbox.data.aspectratio
-
-proc `AspectRatio=`*(textbox: var Textbox, value: float32) =
-  if textbox.shaderdata.data.textbox.data.aspectratio != value:
-    textbox.dirtyShaderdata = true
-    textbox.shaderdata.data.textbox.data.aspectratio = value
-
 proc Position*(textbox: Textbox): Vec3f =
   textbox.shaderdata.data.textbox.data.position
 
@@ -167,8 +160,10 @@
     textbox.verticalAlignment = value
     textbox.dirtyGeometry = true
 
-proc Refresh*(textbox: var Textbox, aspectratio: float32) =
-  `AspectRatio=`(textbox, aspectratio)
+proc Refresh*(textbox: var Textbox) =
+  if textbox.shaderdata.data.textbox.data.aspectratio != GetAspectRatio():
+    textbox.dirtyShaderdata = true
+    textbox.shaderdata.data.textbox.data.aspectratio = GetAspectRatio()
 
   if textbox.dirtyShaderdata:
     textbox.RefreshShaderdata()
@@ -178,8 +173,8 @@
     textbox.RefreshGeometry()
     textbox.dirtyGeometry = false
 
-proc Render*(textbox: Textbox, commandbuffer: VkCommandBuffer, pipeline: Pipeline, currentFiF: int) =
-  WithBind(commandbuffer, (textbox.shaderdata, ), pipeline, currentFiF):
+proc Render*(textbox: Textbox, commandbuffer: VkCommandBuffer, pipeline: Pipeline) =
+  WithBind(commandbuffer, (textbox.shaderdata, ), pipeline):
     Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = textbox)
 
 proc InitTextbox*[T: string | seq[Rune]](
@@ -238,6 +233,6 @@
   UploadImages(renderdata, result.shaderdata)
   InitDescriptorSet(renderdata, descriptorSetLayout, result.shaderdata)
 
-  result.Refresh(1)
-  UpdateAllGPUBuffers(result, flush = true)
+  result.Refresh()
+  UpdateAllGPUBuffers(result, flush = true, allFrames = true)
   UpdateAllGPUBuffers(result.shaderdata.data, flush = true)
--- a/tests/test_rendering.nim	Sun Jul 21 11:31:11 2024 +0700
+++ b/tests/test_rendering.nim	Mon Jul 22 00:46:10 2024 +0700
@@ -7,7 +7,7 @@
 
 import ../semiconginev2
 
-proc test_01_triangle(time: float32, swapchain: var Swapchain) =
+proc test_01_triangle(time: float32) =
   var renderdata = InitRenderData()
 
   type
@@ -33,12 +33,12 @@
   renderdata.FlushAllMemory()
 
   var
-    pipeline = CreatePipeline[TrianglShader](renderPass = swapchain.renderPass)
+    pipeline = CreatePipeline[TrianglShader](renderPass = vulkan.swapchain.renderPass)
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
-    WithNextFrame(swapchain, framebuffer, commandbuffer):
-      WithRenderPass(swapchain.renderPass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+    WithNextFrame(framebuffer, commandbuffer):
+      WithRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
         WithPipeline(commandbuffer, pipeline):
           Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = mesh)
 
@@ -48,7 +48,7 @@
   DestroyRenderData(renderdata)
 
 
-proc test_02_triangle_quad_instanced(time: float32, swapchain: var Swapchain) =
+proc test_02_triangle_quad_instanced(time: float32) =
   var renderdata = InitRenderData()
 
   type
@@ -100,12 +100,12 @@
   AssignBuffers(renderdata, instancesB)
   renderdata.FlushAllMemory()
 
-  var pipeline = CreatePipeline[SomeShader](renderPass = swapchain.renderPass)
+  var pipeline = CreatePipeline[SomeShader](renderPass = vulkan.swapchain.renderPass)
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
-    WithNextFrame(swapchain, framebuffer, commandbuffer):
-      WithRenderPass(swapchain.renderPass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+    WithNextFrame(framebuffer, commandbuffer):
+      WithRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
         WithPipeline(commandbuffer, pipeline):
           Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad, instances = instancesA)
           Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad, instances = instancesB)
@@ -117,7 +117,7 @@
   DestroyPipeline(pipeline)
   DestroyRenderData(renderdata)
 
-proc test_03_simple_descriptorset(time: float32, swapchain: var Swapchain) =
+proc test_03_simple_descriptorset(time: float32) =
   var renderdata = InitRenderData()
 
   type
@@ -176,19 +176,19 @@
   UploadImages(renderdata, uniforms2)
   renderdata.FlushAllMemory()
 
-  var pipeline = CreatePipeline[QuadShader](renderPass = swapchain.renderPass)
+  var pipeline = CreatePipeline[QuadShader](renderPass = vulkan.swapchain.renderPass)
 
   InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], uniforms1)
   InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], uniforms2)
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
-    WithNextFrame(swapchain, framebuffer, commandbuffer):
-      WithRenderPass(swapchain.renderPass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+    WithNextFrame(framebuffer, commandbuffer):
+      WithRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
         WithPipeline(commandbuffer, pipeline):
-          WithBind(commandbuffer, (uniforms1, ), pipeline, swapchain.currentFiF):
+          WithBind(commandbuffer, (uniforms1, ), pipeline):
             Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
-          WithBind(commandbuffer, (uniforms2, ), pipeline, swapchain.currentFiF):
+          WithBind(commandbuffer, (uniforms2, ), pipeline):
             Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
 
   # cleanup
@@ -196,7 +196,7 @@
   DestroyPipeline(pipeline)
   DestroyRenderData(renderdata)
 
-proc test_04_multiple_descriptorsets(time: float32, swapchain: var Swapchain) =
+proc test_04_multiple_descriptorsets(time: float32) =
   var renderdata = InitRenderData()
 
   type
@@ -283,7 +283,7 @@
   UploadImages(renderdata, mainset)
   renderdata.FlushAllMemory()
 
-  var pipeline = CreatePipeline[QuadShader](renderPass = swapchain.renderPass)
+  var pipeline = CreatePipeline[QuadShader](renderPass = vulkan.swapchain.renderPass)
 
   InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], constset)
   InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[1], mainset)
@@ -293,12 +293,12 @@
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     TimeAndLog:
-      WithNextFrame(swapchain, framebuffer, commandbuffer):
-        WithRenderPass(swapchain.renderPass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+      WithNextFrame(framebuffer, commandbuffer):
+        WithRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
           WithPipeline(commandbuffer, pipeline):
-            WithBind(commandbuffer, (constset, mainset, otherset1), pipeline, swapchain.currentFiF):
+            WithBind(commandbuffer, (constset, mainset, otherset1), pipeline):
               Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
-            WithBind(commandbuffer, (constset, mainset, otherset2), pipeline, swapchain.currentFiF):
+            WithBind(commandbuffer, (constset, mainset, otherset2), pipeline):
               Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
     mainset.data.renderSettings.data.brigthness = ((getMonoTime() - start).inMilliseconds().int / 1000) / time
     otherset1.data.objectSettings.data.scale = 0.5 + ((getMonoTime() - start).inMilliseconds().int / 1000) / time
@@ -311,7 +311,7 @@
   DestroyPipeline(pipeline)
   DestroyRenderData(renderdata)
 
-proc test_05_cube(time: float32, swapchain: var Swapchain) =
+proc test_05_cube(time: float32) =
   type
 
     UniformData = object
@@ -405,7 +405,7 @@
 
   renderdata.FlushAllMemory()
 
-  var pipeline = CreatePipeline[CubeShader](renderPass = swapchain.renderPass)
+  var pipeline = CreatePipeline[CubeShader](renderPass = vulkan.swapchain.renderPass)
   InitDescriptorSet(renderdata, pipeline.descriptorSetLayouts[0], uniforms1)
 
   var tStart = getMonoTime()
@@ -416,16 +416,16 @@
     let tStartLoop = getMonoTime() - tStart
 
     uniforms1.data.data.data.mvp = (
-      Perspective(-PI / 2, GetAspectRatio(swapchain), 0.01, 100) *
+      Perspective(-PI / 2, GetAspectRatio(), 0.01, 100) *
       Translate(0, 0, 2) *
       Rotate(PI / 4, X) *
       Rotate(PI * 0.1 * (tStartLoop.inMicroseconds() / 1_000_000), Y)
     )
     UpdateGPUBuffer(uniforms1.data.data, flush = true)
-    WithNextFrame(swapchain, framebuffer, commandbuffer):
-      WithRenderPass(swapchain.renderPass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+    WithNextFrame(framebuffer, commandbuffer):
+      WithRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
         WithPipeline(commandbuffer, pipeline):
-          WithBind(commandbuffer, (uniforms1, ), pipeline, swapchain.currentFiF):
+          WithBind(commandbuffer, (uniforms1, ), pipeline):
             Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = mesh)
             Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = floor)
 
@@ -442,9 +442,9 @@
   DestroyRenderData(renderdata)
 
 proc test_06_triangle_2pass(time: float32, depthBuffer: bool, samples: VkSampleCountFlagBits) =
-  var
-    (offscreenRP, presentRP) = CreateIndirectPresentationRenderPass(depthBuffer = depthBuffer, samples = samples)
-    swapchain = InitSwapchain(renderpass = presentRP).get()
+  var (offscreenRP, presentRP) = CreateIndirectPresentationRenderPass(depthBuffer = depthBuffer, samples = samples)
+
+  SetupSwapchain(renderpass = presentRP)
 
   var renderdata = InitRenderData()
 
@@ -499,7 +499,7 @@
   )
   var uniforms1 = asDescriptorSet(
     Uniforms(
-      frameTexture: Image[TVec4[uint8]](width: swapchain.width, height: swapchain.height, isRenderTarget: true),
+      frameTexture: Image[TVec4[uint8]](width: vulkan.swapchain.width, height: vulkan.swapchain.height, isRenderTarget: true),
     )
   )
   AssignBuffers(renderdata, mesh)
@@ -520,8 +520,8 @@
     depthMemory: VkDeviceMemory
   if offscreenRP.depthBuffer:
     depthImage = svkCreate2DImage(
-      width = swapchain.width,
-      height = swapchain.height,
+      width = vulkan.swapchain.width,
+      height = vulkan.swapchain.height,
       format = DEPTH_FORMAT,
       usage = [VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT],
       samples = offscreenRP.samples,
@@ -550,8 +550,8 @@
     msaaMemory: VkDeviceMemory
   if offscreenRP.samples != VK_SAMPLE_COUNT_1_BIT:
     msaaImage = svkCreate2DImage(
-      width = swapchain.width,
-      height = swapchain.height,
+      width = vulkan.swapchain.width,
+      height = vulkan.swapchain.height,
       format = SURFACE_FORMAT,
       usage = [VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT],
       samples = offscreenRP.samples,
@@ -583,8 +583,8 @@
   echo attachments
   var offscreenFB = svkCreateFramebuffer(
     offscreenRP.vk,
-    swapchain.width,
-    swapchain.height,
+    vulkan.swapchain.width,
+    vulkan.swapchain.height,
     attachments
   )
 
@@ -592,15 +592,15 @@
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
 
     TimeAndLog:
-      WithNextFrame(swapchain, framebuffer, commandbuffer):
+      WithNextFrame(framebuffer, commandbuffer):
 
-        WithRenderPass(offscreenRP, offscreenFB, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+        WithRenderPass(offscreenRP, offscreenFB, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
           WithPipeline(commandbuffer, drawPipeline):
             Render(commandbuffer = commandbuffer, pipeline = drawPipeline, mesh = mesh)
 
-        WithRenderPass(presentRP, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+        WithRenderPass(presentRP, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
           WithPipeline(commandbuffer, presentPipeline):
-            WithBind(commandbuffer, (uniforms1, ), presentPipeline, swapchain.currentFiF):
+            WithBind(commandbuffer, (uniforms1, ), presentPipeline):
               Render(commandbuffer = commandbuffer, pipeline = presentPipeline, mesh = quad)
 
   # cleanup
@@ -619,7 +619,7 @@
   vkDestroyRenderPass(vulkan.device, offscreenRP.vk, nil)
   vkDestroyRenderPass(vulkan.device, presentRP.vk, nil)
   vkDestroyFramebuffer(vulkan.device, offscreenFB, nil)
-  DestroySwapchain(swapchain)
+  ClearSwapchain()
 
 when isMainModule:
   var time = 1'f32
@@ -636,26 +636,26 @@
   # test normal
   for i, (depthBuffer, samples) in renderPasses:
     var renderpass = CreateDirectPresentationRenderPass(depthBuffer = depthBuffer, samples = samples)
-    var swapchain = InitSwapchain(renderpass = renderpass).get()
+    SetupSwapchain(renderpass = renderpass)
 
     # tests a simple triangle with minimalistic shader and vertex format
-    test_01_triangle(time, swapchain)
+    test_01_triangle(time)
 
     # tests instanced triangles and quads, mixing meshes and instances
-    test_02_triangle_quad_instanced(time, swapchain)
+    test_02_triangle_quad_instanced(time)
 
     # teste descriptor sets
-    test_03_simple_descriptorset(time, swapchain)
+    test_03_simple_descriptorset(time)
 
     # tests multiple descriptor sets and arrays
-    test_04_multiple_descriptorsets(time, swapchain)
+    test_04_multiple_descriptorsets(time)
 
     # rotating cube
-    test_05_cube(time, swapchain)
+    test_05_cube(time)
 
     checkVkResult vkDeviceWaitIdle(vulkan.device)
     vkDestroyRenderPass(vulkan.device, renderpass.vk, nil)
-    DestroySwapchain(swapchain)
+    ClearSwapchain()
 
   # test multiple render passes
   for i, (depthBuffer, samples) in renderPasses:
--- a/tests/test_text.nim	Sun Jul 21 11:31:11 2024 +0700
+++ b/tests/test_text.nim	Mon Jul 22 00:46:10 2024 +0700
@@ -8,10 +8,10 @@
 
 import ../semiconginev2
 
-proc test_01_static_label(time: float32, swapchain: var Swapchain) =
+proc test_01_static_label(time: float32) =
   var renderdata = InitRenderData()
 
-  var pipeline = CreatePipeline[DefaultFontShader](renderPass = swapchain.renderPass)
+  var pipeline = CreatePipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass)
 
   var font = LoadFont("Overhaul.ttf", lineHeightPixels = 160)
   var label1 = InitTextbox(
@@ -25,21 +25,21 @@
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
-    label1.Refresh(swapchain.GetAspectRatio())
-    WithNextFrame(swapchain, framebuffer, commandbuffer):
-      WithRenderPass(swapchain.renderPass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+    label1.Refresh()
+    WithNextFrame(framebuffer, commandbuffer):
+      WithRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
         WithPipeline(commandbuffer, pipeline):
-          Render(label1, commandbuffer, pipeline, swapchain.currentFiF)
+          Render(label1, commandbuffer, pipeline)
 
         # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
   DestroyPipeline(pipeline)
   DestroyRenderData(renderdata)
 
-proc test_02_multiple_animated(time: float32, swapchain: var Swapchain) =
+proc test_02_multiple_animated(time: float32) =
   var renderdata = InitRenderData()
 
-  var pipeline = CreatePipeline[DefaultFontShader](renderPass = swapchain.renderPass)
+  var pipeline = CreatePipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass)
 
   var font1 = LoadFont("Overhaul.ttf", lineHeightPixels = 40)
   var font2 = LoadFont("Overhaul.ttf", lineHeightPixels = 160)
@@ -85,22 +85,81 @@
       labels[i].Scale = labels[i].Scale * (1.0 + (i + 1).float * 0.001)
       labels[i].Position = labels[i].Position + NewVec3f(0.001 * (i.float - 1'f))
       labels[i].text = $(p + i)
-      labels[i].Refresh(swapchain.GetAspectRatio())
+      labels[i].Refresh()
     inc p
-    WithNextFrame(swapchain, framebuffer, commandbuffer):
-      WithRenderPass(swapchain.renderPass, framebuffer, commandbuffer, swapchain.width, swapchain.height, NewVec4f(0, 0, 0, 0)):
+    WithNextFrame(framebuffer, commandbuffer):
+      WithRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
         WithPipeline(commandbuffer, pipeline):
           for label in labels:
-            Render(label, commandbuffer, pipeline, swapchain.currentFiF)
+            Render(label, commandbuffer, pipeline)
 
-        # cleanup
+      # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
   DestroyPipeline(pipeline)
   DestroyRenderData(renderdata)
 
-proc test_03_layouting(time: float32, swapchain: var Swapchain) =
-  discard # TODO
-proc test_04_lots_of_texts(time: float32, swapchain: var Swapchain) =
+proc test_03_layouting(time: float32) =
+  var renderdata = InitRenderData()
+
+  var pipeline = CreatePipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass)
+
+  var font = LoadFont("DejaVuSans.ttf", lineHeightPixels = 40)
+  var labels: seq[Textbox]
+
+  for horizontal in HorizontalAlignment:
+    labels.add InitTextbox(
+      renderdata,
+      pipeline.descriptorSetLayouts[0],
+      font,
+      $horizontal & " aligned",
+      color = NewVec4f(1, 1, 1, 1),
+      scale = 0.001,
+      position = NewVec3f(0, 0.9 - (horizontal.float * 0.15)),
+      horizontalAlignment = horizontal,
+    )
+  for vertical in VerticalAlignment:
+    labels.add InitTextbox(
+      renderdata,
+      pipeline.descriptorSetLayouts[0],
+      font,
+      $vertical & " aligned",
+      color = NewVec4f(1, 1, 1, 1),
+      scale = 0.001,
+      position = NewVec3f(-0.35 + (vertical.float * 0.35), 0.3),
+      verticalAlignment = vertical,
+    )
+  labels.add InitTextbox(
+    renderdata,
+    pipeline.descriptorSetLayouts[0],
+    font,
+    """Paragraph
+This is a somewhat longer paragraph with a few newlines and a maximum width of 0.2.
+
+It should display with some space above and have a pleasing appearance overall! :)""",
+    maxWidth = 0.6,
+    color = NewVec4f(1, 1, 1, 1),
+    scale = 0.001,
+    position = NewVec3f(-0.9, 0.1),
+    verticalAlignment = Top,
+    horizontalAlignment = Left,
+  )
+
+
+  var start = getMonoTime()
+  while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
+    let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time
+    WithNextFrame(framebuffer, commandbuffer):
+      WithRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, NewVec4f(0, 0, 0, 0)):
+        WithPipeline(commandbuffer, pipeline):
+          for label in labels:
+            Render(label, commandbuffer, pipeline)
+
+      # cleanup
+  checkVkResult vkDeviceWaitIdle(vulkan.device)
+  DestroyPipeline(pipeline)
+  DestroyRenderData(renderdata)
+
+proc test_04_lots_of_texts(time: float32) =
   discard # TODO
 
 when isMainModule:
@@ -108,14 +167,15 @@
   InitVulkan()
 
   var renderpass = CreateDirectPresentationRenderPass(depthBuffer = true)
-  var swapchain = InitSwapchain(renderpass = renderpass).get()
+  SetupSwapchain(renderpass = renderpass)
 
   # tests a simple triangle with minimalistic shader and vertex format
   # test_01_static_label(time, swapchain)
-  test_02_multiple_animated(time, swapchain)
+  # test_02_multiple_animated(time)
+  test_03_layouting(time)
+
 
   checkVkResult vkDeviceWaitIdle(vulkan.device)
   vkDestroyRenderPass(vulkan.device, renderpass.vk, nil)
-  DestroySwapchain(swapchain)
 
   DestroyVulkan()