changeset 1327:373a4888f6ac

did: rework font-rendering
author sam <sam@basx.dev>
date Sat, 17 Aug 2024 13:54:22 +0700
parents 41f3612ef38d
children a53a31b6e027
files semicongine/rendering.nim semicongine/rendering/renderer.nim semicongine/text.nim semicongine/text/textbox.nim tests/test_text.nim
diffstat 5 files changed, 156 insertions(+), 143 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/rendering.nim	Sat Aug 17 11:34:15 2024 +0700
+++ b/semicongine/rendering.nim	Sat Aug 17 13:54:22 2024 +0700
@@ -159,6 +159,11 @@
 proc `=copy`[T](dest: var Pipeline[T]; source: Pipeline[T]) {.error.}
 proc `=copy`[T](dest: var DescriptorSetData[T]; source: DescriptorSetData[T]) {.error.}
 
+proc `[]`*[T, S](a: GPUArray[T, S], i: int): T =
+  a.data[i]
+proc `[]=`*[T, S](a: var GPUArray[T, S], i: int, value: T) =
+  a.data[i] = value
+
 template forDescriptorFields(shader: typed, valuename, typename, countname, bindingNumber, body: untyped): untyped =
   var `bindingNumber` {.inject.} = 0'u32
   for theFieldname, `valuename` in fieldPairs(shader):
--- a/semicongine/rendering/renderer.nim	Sat Aug 17 11:34:15 2024 +0700
+++ b/semicongine/rendering/renderer.nim	Sat Aug 17 13:54:22 2024 +0700
@@ -52,8 +52,8 @@
 template needsMapping(gpuData: GPUData): untyped =
   gpuData.bufferType.needsMapping
 
-template size(gpuArray: GPUArray): uint64 =
-  (gpuArray.data.len * sizeof(elementType(gpuArray.data))).uint64
+template size(gpuArray: GPUArray, count=0'u64): uint64 =
+  (if count == 0: gpuArray.data.len.uint64 else: count).uint64 * sizeof(elementType(gpuArray.data)).uint64
 template size(gpuValue: GPUValue): uint64 =
   sizeof(gpuValue.data).uint64
 func size(image: Image): uint64 =
@@ -276,17 +276,23 @@
   result.rawPointer = selectedBlock.rawPointer.pointerAddOffset(selectedBlock.offsetNextFree)
   renderData.memory[memoryType][selectedBlockI].offsetNextFree += memoryRequirements.size
 
-proc updateGPUBuffer*(gpuData: GPUData, flush = false) =
-  if gpuData.size == 0:
+proc updateGPUBuffer*(gpuData: GPUData, count=0'u64, flush = false) =
+  if gpuData.size() == 0:
     return
 
   when needsMapping(gpuData):
-    copyMem(pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset), gpuData.rawPointer, gpuData.size)
+    when gpuData is GPUArray:
+      copyMem(pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset), gpuData.rawPointer, gpuData.size(count))
+    else:
+      copyMem(pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset), gpuData.rawPointer, gpuData.size())
     if flush:
       flushBuffer(gpuData.buffer)
   else:
     withStagingBuffer((gpuData.buffer.vk, gpuData.offset), gpuData.size, stagingPtr):
-      copyMem(stagingPtr, gpuData.rawPointer, gpuData.size)
+      when gpuData is GPUArray:
+        copyMem(stagingPtr, gpuData.rawPointer, gpuData.size(count))
+      else:
+        copyMem(stagingPtr, gpuData.rawPointer, gpuData.size())
 
 proc updateAllGPUBuffers*[T](value: T, flush = false) =
   for name, fieldvalue in value.fieldPairs():
--- a/semicongine/text.nim	Sat Aug 17 11:34:15 2024 +0700
+++ b/semicongine/text.nim	Sat Aug 17 13:54:22 2024 +0700
@@ -43,19 +43,16 @@
     color: Vec4f
     position: Vec3f
     scale: float32
-    aspectratio: float32
-  TextboxDescriptorSet = object
-    textbox: GPUValue[TextboxData, UniformBufferMapped]
-    fontAtlas: Image[Gray]
 
-  DefaultFontShader* = object
+  DefaultFontShader*[T] = object
     position {.VertexAttribute.}: Vec3f
     uv {.VertexAttribute.}: Vec2f # TODO: maybe we can keep the uvs in a uniform buffer and just pass an index
     fragmentUv {.Pass.}: Vec2f
     color {.ShaderOutput.}: Vec4f
-    descriptorSets {.DescriptorSet: 0.}: TextboxDescriptorSet
+    textbox {.PushConstant.}: TextboxData
+    descriptorSets {.DescriptorSet: 0.}: T
     vertexCode* = """void main() {
-  gl_Position = vec4(position * vec3(1 / textbox.aspectratio, 1, 1) * textbox.scale + textbox.position, 1.0);
+  gl_Position = vec4(position * textbox.scale + textbox.position, 1.0);
   fragmentUv = uv;
 }  """
     fragmentCode* = """void main() {
@@ -66,6 +63,7 @@
     color = vec4(textbox.color.rgb, textbox.color.a * v);
 }"""
 
+proc `=copy`(dest: var FontObj; source: FontObj) {.error.}
 
 include ./text/font
 include ./text/textbox
--- a/semicongine/text/textbox.nim	Sat Aug 17 11:34:15 2024 +0700
+++ b/semicongine/text/textbox.nim	Sat Aug 17 13:54:22 2024 +0700
@@ -3,43 +3,39 @@
     font*: Font
     maxLen*: int                # maximum amount of characters that will be rendered
     maxWidth: float32 = 0       # if set, will cause automatic word breaks at maxWidth
-                                # properties:
+    baseScale: float32
     text: seq[Rune]
     horizontalAlignment: HorizontalAlignment = Center
     verticalAlignment: VerticalAlignment = Center
     # management/internal:
     dirtyGeometry: bool         # is true if any of the attributes changed
     dirtyShaderdata: bool       # is true if any of the attributes changed
-    processedText: seq[Rune]    # used to store processed (word-wrapper) text to preserve original
+    visibleText: seq[Rune]    # used to store processed (word-wrapper) text to preserve original
     lastRenderedText: seq[Rune] # stores the last rendered text, to prevent unnecessary updates
 
     # rendering data
     position: GPUArray[Vec3f, VertexBuffer]
     uv: GPUArray[Vec2f, VertexBuffer]
     indices: GPUArray[uint16, IndexBuffer]
-    shaderdata: DescriptorSetData[TextboxDescriptorSet]
 
 proc `=copy`(dest: var Textbox; source: Textbox) {.error.}
 
 func `$`*(textbox: Textbox): string =
   "\"" & $textbox.text[0 ..< min(textbox.text.len, 16)] & "\""
 
-proc refreshShaderdata(textbox: Textbox) =
-  textbox.shaderdata.data.textbox.updateGPUBuffer(flush = true)
-
 proc refreshGeometry(textbox: var Textbox) =
   # pre-calculate text-width
   var width = 0'f32
   var lineWidths: seq[float32]
-  for i in 0 ..< textbox.processedText.len:
-    if textbox.processedText[i] == NEWLINE:
+  for i in 0 ..< textbox.visibleText.len:
+    if textbox.visibleText[i] == NEWLINE:
       lineWidths.add width
       width = 0'f32
     else:
-      if not (i == textbox.processedText.len - 1 and textbox.processedText[i].isWhiteSpace):
-        width += textbox.font.glyphs[textbox.processedText[i]].advance
-      if i < textbox.processedText.len - 1:
-        width += textbox.font.kerning[(textbox.processedText[i], textbox.processedText[i + 1])]
+      if not (i == textbox.visibleText.len - 1 and textbox.visibleText[i].isWhiteSpace):
+        width += textbox.font.glyphs[textbox.visibleText[i]].advance
+      if i < textbox.visibleText.len - 1:
+        width += textbox.font.kerning[(textbox.visibleText[i], textbox.visibleText[i + 1])]
   lineWidths.add width
   var height = float32(lineWidths.len - 1) * textbox.font.lineAdvance + textbox.font.capHeight
   if lineWidths[^1] == 0 and lineWidths.len > 1:
@@ -61,8 +57,8 @@
       of Right: lineWidths[lineIndex]
   for i in 0 ..< textbox.maxLen:
     let vertexOffset = i * 4
-    if i < textbox.processedText.len:
-      if textbox.processedText[i] == Rune('\n'):
+    if i < textbox.visibleText.len:
+      if textbox.visibleText[i] == Rune('\n'):
         offsetX = 0
         offsetY -= textbox.font.lineAdvance
         textbox.position.data[vertexOffset + 0] = vec3(0, 0, 0)
@@ -76,7 +72,7 @@
           of Right: lineWidths[lineIndex]
       else:
         let
-          glyph = textbox.font.glyphs[textbox.processedText[i]]
+          glyph = textbox.font.glyphs[textbox.visibleText[i]]
           left = offsetX + glyph.leftOffset
           right = offsetX + glyph.leftOffset + glyph.dimension.x
           top = offsetY - glyph.topOffset
@@ -93,16 +89,11 @@
         textbox.uv.data[vertexOffset + 3] = glyph.uvs[3]
 
         offsetX += glyph.advance
-        if i < textbox.processedText.len - 1:
-          offsetX += textbox.font.kerning[(textbox.processedText[i], textbox.processedText[i + 1])]
-    else:
-      textbox.position.data[vertexOffset + 0] = vec3(0, 0, 0)
-      textbox.position.data[vertexOffset + 1] = vec3(0, 0, 0)
-      textbox.position.data[vertexOffset + 2] = vec3(0, 0, 0)
-      textbox.position.data[vertexOffset + 3] = vec3(0, 0, 0)
-  updateGPUBuffer(textbox.position)
-  updateGPUBuffer(textbox.uv)
-  textbox.lastRenderedText = textbox.processedText
+        if i < textbox.visibleText.len - 1:
+          offsetX += textbox.font.kerning[(textbox.visibleText[i], textbox.visibleText[i + 1])]
+  updateGPUBuffer(textbox.position, count=textbox.visibleText.len.uint64 * 4)
+  updateGPUBuffer(textbox.uv, count=textbox.visibleText.len.uint64 * 4)
+  textbox.lastRenderedText = textbox.visibleText
 
 func text*(textbox: Textbox): seq[Rune] =
   textbox.text
@@ -113,41 +104,17 @@
 
   textbox.text = newText[0 ..< min(newText.len, textbox.maxLen)]
 
-  textbox.processedText = textbox.text
+  textbox.visibleText = textbox.text
   if textbox.maxWidth > 0:
-    textbox.processedText = WordWrapped(
-      textbox.processedText,
+    textbox.visibleText = WordWrapped(
+      textbox.visibleText,
       textbox.font[],
-      textbox.maxWidth / textbox.shaderdata.data.textbox.data.scale,
+      textbox.maxWidth / textbox.baseScale,
     )
 
 proc `text=`*(textbox: var Textbox, newText: string) =
   `text=`(textbox, newText.toRunes)
 
-proc color*(textbox: Textbox): Vec4f =
-  textbox.shaderdata.data.textbox.data.color
-
-proc `color=`*(textbox: var Textbox, value: Vec4f) =
-  if textbox.shaderdata.data.textbox.data.color != value:
-    textbox.dirtyShaderdata = true
-    textbox.shaderdata.data.textbox.data.color = value
-
-proc scale*(textbox: Textbox): float32 =
-  textbox.shaderdata.data.textbox.data.scale
-
-proc `scale=`*(textbox: var Textbox, value: float32) =
-  if textbox.shaderdata.data.textbox.data.scale != value:
-    textbox.dirtyShaderdata = true
-    textbox.shaderdata.data.textbox.data.scale = value
-
-proc position*(textbox: Textbox): Vec3f =
-  textbox.shaderdata.data.textbox.data.position
-
-proc `position=`*(textbox: var Textbox, value: Vec3f) =
-  if textbox.shaderdata.data.textbox.data.position != value:
-    textbox.dirtyShaderdata = true
-    textbox.shaderdata.data.textbox.data.position = value
-
 proc horizontalAlignment*(textbox: Textbox): HorizontalAlignment =
   textbox.horizontalAlignment
 proc `horizontalAlignment=`*(textbox: var Textbox, value: HorizontalAlignment) =
@@ -163,34 +130,36 @@
     textbox.dirtyGeometry = true
 
 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()
-    textbox.dirtyShaderdata = false
-
-  if textbox.dirtyGeometry or textbox.processedText != textbox.lastRenderedText:
+  if textbox.dirtyGeometry or textbox.visibleText != textbox.lastRenderedText:
     textbox.refreshGeometry()
     textbox.dirtyGeometry = false
 
-proc render*(commandbuffer: VkCommandBuffer, pipeline: Pipeline, textbox: Textbox) =
-  bindDescriptorSet(commandbuffer, textbox.shaderdata, 0, pipeline)
-  render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = textbox)
+proc render*(
+  commandbuffer: VkCommandBuffer,
+  pipeline: Pipeline,
+  textbox: Textbox,
+  position: Vec3f,
+  color: Vec4f,
+  scale: float32 = 1,
+) =
+  renderWithPushConstant(
+    commandbuffer = commandbuffer,
+    pipeline = pipeline,
+    mesh = textbox,
+    pushConstant = TextboxData(position: position, scale: textbox.baseScale * scale, color: color),
+    fixedVertexCount=textbox.visibleText.len * 6
+  )
 
 proc initTextbox*[T: string | seq[Rune]](
   renderdata: var RenderData,
   descriptorSetLayout: VkDescriptorSetLayout,
   font: Font,
+  baseScale: float32,
   text: T = default(T),
-  scale: float32 = 1,
-  position: Vec3f = vec3(0, 0, 0),
-  color: Vec4f = vec4(0, 0, 0, 1),
   maxLen: int = text.len,
   verticalAlignment: VerticalAlignment = Center,
   horizontalAlignment: HorizontalAlignment = Center,
-  maxWidth = 0'f32
+  maxWidth = 0'f32,
 ): Textbox =
 
   result = Textbox(
@@ -201,20 +170,10 @@
     horizontalAlignment: horizontalAlignment,
     verticalAlignment: verticalAlignment,
     maxWidth: maxWidth,
+    baseScale: baseScale,
     position: asGPUArray(newSeq[Vec3f](int(maxLen * 4)), VertexBuffer),
     uv: asGPUArray(newSeq[Vec2f](int(maxLen * 4)), VertexBuffer),
     indices: asGPUArray(newSeq[uint16](int(maxLen * 6)), IndexBuffer),
-    shaderdata: asDescriptorSetData(
-      TextboxDescriptorSet(
-        textbox: asGPUValue(TextboxData(
-          scale: scale,
-          position: position,
-          color: color,
-          aspectratio: 1,
-    ), UniformBufferMapped),
-    fontAtlas: font.fontAtlas
-  )
-    )
   )
 
   for i in 0 ..< maxLen:
@@ -232,9 +191,6 @@
     `text=`(result, text)
 
   assignBuffers(renderdata, result, uploadData = false)
-  uploadImages(renderdata, result.shaderdata)
-  initDescriptorSet(renderdata, descriptorSetLayout, result.shaderdata)
 
   result.refresh()
   updateAllGPUBuffers(result, flush = true)
-  updateAllGPUBuffers(result.shaderdata.data, flush = true)
--- a/tests/test_text.nim	Sat Aug 17 11:34:15 2024 +0700
+++ b/tests/test_text.nim	Sat Aug 17 13:54:22 2024 +0700
@@ -9,28 +9,39 @@
 
 import ../semicongine
 
+type
+  FontDS = object
+    fontAtlas: Image[Gray]
+
 proc test_01_static_label(time: float32) =
+  var font = loadFont("Overhaul.ttf", lineHeightPixels = 160)
   var renderdata = initRenderData()
+  var pipeline = createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass)
 
-  var pipeline = createPipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass)
+  var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas))
+  uploadImages(renderdata, ds)
+  initDescriptorSet(
+    renderdata,
+    pipeline.layout(0),
+    ds,
+  )
 
-  var font = loadFont("Overhaul.ttf", lineHeightPixels = 160)
   var label1 = initTextbox(
     renderdata,
     pipeline.layout(0),
     font,
+    0.0005,
     "Hello semicongine!",
-    color = vec4(1, 1, 1, 1),
-    scale = 0.0005,
   )
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     label1.refresh()
     withNextFrame(framebuffer, commandbuffer):
+      bindDescriptorSet(commandbuffer, ds, 0, pipeline)
       withRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, vec4(0, 0, 0, 0)):
         withPipeline(commandbuffer, pipeline):
-          render(commandbuffer, pipeline, label1)
+          render(commandbuffer, pipeline, label1, vec3(), vec4(1, 1, 1, 1))
 
         # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
@@ -38,40 +49,46 @@
   destroyRenderData(renderdata)
 
 proc test_02_multiple_animated(time: float32) =
-  var renderdata = initRenderData()
-
-  var pipeline = createPipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass)
-
   var font1 = loadFont("Overhaul.ttf", lineHeightPixels = 40)
   var font2 = loadFont("Overhaul.ttf", lineHeightPixels = 160)
   var font3 = loadFont("DejaVuSans.ttf", lineHeightPixels = 160)
+  var renderdata = initRenderData()
+
+  var pipeline = createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass)
+
+  var ds1 = asDescriptorSetData(FontDS(fontAtlas: font1.fontAtlas))
+  uploadImages(renderdata, ds1)
+  initDescriptorSet(renderdata, pipeline.layout(0), ds1)
+
+  var ds2 = asDescriptorSetData(FontDS(fontAtlas: font2.fontAtlas))
+  uploadImages(renderdata, ds2)
+  initDescriptorSet(renderdata, pipeline.layout(0), ds2)
+
+  var ds3 = asDescriptorSetData(FontDS(fontAtlas: font3.fontAtlas))
+  uploadImages(renderdata, ds3)
+  initDescriptorSet(renderdata, pipeline.layout(0), ds3)
+
   var labels = [
     initTextbox(
       renderdata,
       pipeline.layout(0),
       font1,
+      0.004,
       "  0",
-      color = vec4(0, 1, 1, 1),
-      scale = 0.004,
-      position = vec3(-0.3, 0.5)
     ),
     initTextbox(
       renderdata,
       pipeline.layout(0),
       font2,
+      0.001,
       "  1",
-      color = vec4(1, 0, 1, 1),
-      scale = 0.001,
-      position = vec3(0, 0)
     ),
     initTextbox(
       renderdata,
       pipeline.layout(0),
       font3,
+      0.001,
       "  2",
-      color = vec4(1, 1, 0, 1),
-      scale = 0.001,
-      position = vec3(0.3, -0.5)
     )
   ]
 
@@ -80,19 +97,36 @@
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time
     for i in 0 ..< labels.len:
-      var c = labels[i].color
-      c[i] = progress
-      labels[i].color = c
-      labels[i].scale = labels[i].scale * (1.0 + (i + 1).float * 0.001)
-      labels[i].position = labels[i].position + vec3(0.001 * (i.float - 1'f))
       labels[i].text = $(p + i)
       labels[i].refresh()
     inc p
     withNextFrame(framebuffer, commandbuffer):
       withRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, vec4(0, 0, 0, 0)):
         withPipeline(commandbuffer, pipeline):
-          for label in labels.litems:
-            render(commandbuffer, pipeline, label)
+          bindDescriptorSet(commandbuffer, ds1, 0, pipeline)
+          render(
+            commandbuffer,
+            pipeline,
+            labels[0],
+            position=vec3(0 / labels.len, 0.1 + progress * 0.5),
+            color=vec4(1, 1, 1, 1),
+          )
+          bindDescriptorSet(commandbuffer, ds2, 0, pipeline)
+          render(
+            commandbuffer,
+            pipeline,
+            labels[1],
+            position=vec3(1 / labels.len, 0.1 + progress * 0.5),
+            color=vec4(1, 1, 1, 1),
+          )
+          bindDescriptorSet(commandbuffer, ds3, 0, pipeline)
+          render(
+            commandbuffer,
+            pipeline,
+            labels[2],
+            position=vec3(2 / labels.len, 0.1 + progress * 0.5),
+            color=vec4(1, 1, 1, 1),
+          )
 
       # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
@@ -100,11 +134,19 @@
   destroyRenderData(renderdata)
 
 proc test_03_layouting(time: float32) =
+  var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 40)
   var renderdata = initRenderData()
 
-  var pipeline = createPipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass)
+  var pipeline = createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass)
 
-  var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 40)
+  var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas))
+  uploadImages(renderdata, ds)
+  initDescriptorSet(
+    renderdata,
+    pipeline.layout(0),
+    ds,
+  )
+
   var labels: seq[Textbox]
 
   for horizontal in HorizontalAlignment:
@@ -112,10 +154,8 @@
       renderdata,
       pipeline.layout(0),
       font,
+      0.001,
       $horizontal & " aligned",
-      color = vec4(1, 1, 1, 1),
-      scale = 0.001,
-      position = vec3(0, 0.9 - (horizontal.float * 0.15)),
       horizontalAlignment = horizontal,
     )
   for vertical in VerticalAlignment:
@@ -123,37 +163,33 @@
       renderdata,
       pipeline.layout(0),
       font,
+      0.001,
       $vertical & " aligned",
-      color = vec4(1, 1, 1, 1),
-      scale = 0.001,
-      position = vec3(-0.35 + (vertical.float * 0.35), 0.3),
       verticalAlignment = vertical,
     )
   labels.add initTextbox(
     renderdata,
     pipeline.layout(0),
     font,
+    0.001,
     """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 = vec4(1, 1, 1, 1),
-    scale = 0.001,
-    position = vec3(-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):
+      bindDescriptorSet(commandbuffer, ds, 0, pipeline)
       withRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, vec4(0, 0, 0, 0)):
         withPipeline(commandbuffer, pipeline):
-          for label in labels:
-            render(commandbuffer, pipeline, label)
+          for i in 0 ..< labels.len:
+            render(commandbuffer, pipeline, labels[i], vec3(0.5 - i.float32 * 0.1, 0.5 - i.float32 * 0.1), vec4(1, 1, 1, 1))
 
       # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
@@ -161,33 +197,45 @@
   destroyRenderData(renderdata)
 
 proc test_04_lots_of_texts(time: float32) =
+  var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 160)
   var renderdata = initRenderData()
 
-  var pipeline = createPipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass)
+  var pipeline = createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass)
 
-  var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 160)
+  var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas))
+  uploadImages(renderdata, ds)
+  initDescriptorSet(
+    renderdata,
+    pipeline.layout(0),
+    ds,
+  )
+
   var labels: seq[Textbox]
+  var positions = newSeq[Vec3f](100)
+  var colors = newSeq[Vec4f](100)
+  var scales = newSeq[float32](100)
   for i in 0 ..< 100:
+    positions[i] = vec3(rand(-0.5 .. 0.5), rand(-0.5 .. 0.5), rand(-0.1 .. 0.1))
+    colors[i] = vec4(rand(0.5 .. 1.0), rand(0.5 .. 1.0), rand(0.5 .. 1.0), rand(0.5 .. 1.0))
+    scales[i] = rand(0.5'f32 .. 1.5'f32)
     labels.add initTextbox(
       renderdata,
       pipeline.layout(0),
       font,
+      0.001,
       $i,
-      color = vec4(rand(0.5 .. 1.0), rand(0.5 .. 1.0), rand(0.5 .. 1.0), rand(0.5 .. 1.0)),
-      scale = rand(0.0002 .. 0.002),
-      position = vec3(rand(-0.5 .. 0.5), rand(-0.5 .. 0.5), rand(-0.1 .. 0.1))
     )
-  labels.sort(proc(x, y: Textbox): int = cmp(x.position.z, y.position.z), Ascending)
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     for l in labels.mitems:
       l.refresh()
     withNextFrame(framebuffer, commandbuffer):
+      bindDescriptorSet(commandbuffer, ds, 0, pipeline)
       withRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, vec4(0, 0, 0, 0)):
         withPipeline(commandbuffer, pipeline):
-          for l in labels:
-            render(commandbuffer, pipeline, l)
+          for i in 0 ..< labels.len:
+            render(commandbuffer, pipeline, labels[i], positions[i], colors[i], scales[i])
 
         # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)