changeset 1407:56f927b89716

did: finally got typography right, still improving text-rendering API to cache text parts
author sam <sam@basx.dev>
date Sun, 22 Dec 2024 00:31:29 +0700
parents aeb15aa9768c
children 17d960ff6a24
files semicongine/text.nim semicongine/text/font.nim tests/test_text.nim
diffstat 3 files changed, 158 insertions(+), 120 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/text.nim	Sat Dec 21 19:32:59 2024 +0700
+++ b/semicongine/text.nim	Sun Dec 22 00:31:29 2024 +0700
@@ -25,40 +25,51 @@
     Center
     Right
 
-  GlyphQuad[N: static int] = object
-    pos: array[N, Vec4f] # vertex offsets to glyph center: [left, bottom, right, top]
-    uv: array[N, Vec4f] # [left, bottom, right, top]
+  GlyphQuad[MaxGlyphs: static int] = object
+    pos: array[MaxGlyphs, Vec4f]
+      # vertex offsets to glyph center: [left, bottom, right, top]
+    uv: array[MaxGlyphs, Vec4f] # [left, bottom, right, top]
 
-  GlyphDescriptorSet*[N: static int] = object
+  GlyphDescriptorSet*[MaxGlyphs: static int] = object
     fontAtlas*: Image[Gray]
-    glyphquads*: GPUValue[GlyphQuad[N], StorageBuffer]
+    glyphquads*: GPUValue[GlyphQuad[MaxGlyphs], StorageBuffer]
 
-  FontObj*[N: static int] = object
+  FontObj*[MaxGlyphs: static int] = object
     advance*: Table[Rune, float32]
     kerning*: Table[(Rune, Rune), float32]
     lineAdvance*: float32
     lineHeight*: float32 # like lineAdvance - lineGap
     ascent*: float32 # from baseline to highest glyph
     descent*: float32 # from baseline to highest glyph
-    descriptorSet*: DescriptorSetData[GlyphDescriptorSet[N]]
+    xHeight*: float32 # from baseline to height of lowercase x
+    descriptorSet*: DescriptorSetData[GlyphDescriptorSet[MaxGlyphs]]
     descriptorGlyphIndex: Table[Rune, uint16]
     fallbackCharacter: Rune
 
-  Font*[N: static int] = ref FontObj[N]
+  Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs]
+  Text = object
+    bufferOffset: int
+    text: seq[Rune]
+    position: Vec3f = vec3()
+    alignment: TextAlignment = Left
+    anchor: Vec2f = vec2()
+    scale: float32 = 0
+    color: Vec4f = vec4(1, 1, 1, 1)
 
-  Glyphs*[N: static int] = object
+  TextBuffer*[MaxGlyphs: static int] = object
     cursor: int
-    font*: Font[N]
+    font*: Font[MaxGlyphs]
     baseScale*: float32
     position*: GPUArray[Vec3f, VertexBufferMapped]
     color*: GPUArray[Vec4f, VertexBufferMapped]
     scale*: GPUArray[float32, VertexBufferMapped]
     glyphIndex*: GPUArray[uint16, VertexBufferMapped]
+    texts: seq[Text]
 
   TextRendering* = object
     aspectRatio*: float32
 
-  GlyphShader*[N: static int] = object
+  GlyphShader*[MaxGlyphs: static int] = object
     position {.InstanceAttribute.}: Vec3f
     color {.InstanceAttribute.}: Vec4f
     scale {.InstanceAttribute.}: float32
@@ -68,7 +79,7 @@
     fragmentUv {.Pass.}: Vec2f
     fragmentColor {.PassFlat.}: Vec4f
     outColor {.ShaderOutput.}: Vec4f
-    glyphData {.DescriptorSet: 0.}: GlyphDescriptorSet[N]
+    glyphData {.DescriptorSet: 0.}: GlyphDescriptorSet[MaxGlyphs]
     vertexCode* =
       """
 const int[6] indices = int[](0, 1, 2, 2, 3, 0);
@@ -95,21 +106,27 @@
     outColor = vec4(fragmentColor.rgb, fragmentColor.a * a);
 }"""
 
-proc `=copy`[N: static int](dest: var FontObj[N], source: FontObj[N]) {.error.}
-proc `=copy`[N: static int](dest: var Glyphs[N], source: Glyphs[N]) {.error.}
+proc `=copy`[MaxGlyphs: static int](
+  dest: var FontObj[MaxGlyphs], source: FontObj[MaxGlyphs]
+) {.error.}
+
+proc `=copy`[MaxGlyphs: static int](
+  dest: var TextBuffer[MaxGlyphs], source: TextBuffer[MaxGlyphs]
+) {.error.}
 
 include ./text/font
 
-func initGlyphs*[N: static int](
-    font: Font[N], count: int, baseScale = 1'f32
-): Glyphs[N] =
+func initTextBuffer*[MaxGlyphs: static int](
+    font: Font[MaxGlyphs], maxCharacters: int, baseScale = 1'f32
+): TextBuffer[MaxGlyphs] =
   result.cursor = 0
   result.font = font
   result.baseScale = baseScale
-  result.position.data.setLen(count)
-  result.scale.data.setLen(count)
-  result.color.data.setLen(count)
-  result.glyphIndex.data.setLen(count)
+  result.position.data.setLen(maxCharacters)
+  result.scale.data.setLen(maxCharacters)
+  result.color.data.setLen(maxCharacters)
+  result.glyphIndex.data.setLen(maxCharacters)
+  result.texts.setLen(maxCharacters) # waste a lot of memory?
 
 iterator splitLines(text: seq[Rune]): seq[Rune] =
   var current = newSeq[Rune]()
@@ -140,36 +157,45 @@
   return vec2(w, h)
 
 proc add*(
-    glyphs: var Glyphs,
+    textbuffer: var TextBuffer,
     text: seq[Rune],
     position: Vec3f,
     alignment: TextAlignment = Left,
-    anchor: Vec2f = vec2(-1, 1),
+    anchor: Vec2f = vec2(0, 0),
     scale: float32 = 1'f32,
     color: Vec4f = vec4(1, 1, 1, 1),
 ) =
-  ## Add text for rendering.
-  ## `position` is the display position, where as `(0, 0) is top-left and (1, 1) is bottom right.
-  ## The z-compontent goes from 0 (near plane) to 1 (far plane) and is usually just used for ordering layers
-  ## this should be called again after aspect ratio of window changes 
-  ## Anchor is the anchor to use inside the text
+  ## This should be called again after aspect ratio of window changes 
+
+  assert text.len <= textbuffer.position.len,
+    &"Set {text.len} but TextBuffer-object only supports {textbuffer.position.len}"
 
-  assert text.len <= glyphs.position.len,
-    &"Set {text.len} but Glyphs-object only supports {glyphs.position.len}"
+  textbuffer.texts.add Text(
+    bufferOffset: textbuffer.cursor,
+    text: text,
+    position: position,
+    alignment: alignment,
+    anchor: anchor,
+    scale: scale,
+    color: color,
+  )
 
   let
-    globalScale = scale * glyphs.baseScale
-    dim = textDimension(glyphs.font, text, globalScale)
-    baselineStart = vec2(0, glyphs.font.ascent * globalScale)
-    pos = position.xy - anchor * dim + baselineStart
-    # lineWidths need to be converted to NDC
-    lineWidths = splitLines(text).toSeq.mapIt(width(glyphs.font, it, globalScale))
-    # also dimension must be in NDC
-    maxWidth = dim.x
+    globalScale = scale * textbuffer.baseScale
+    box = textDimension(textbuffer.font, text, globalScale)
+    xH = textbuffer.font.xHeight * globalScale
+    origin = vec3(
+      position.x - (anchor.x * 0.5 + 0.5) * box.x / getAspectRatio(),
+      position.y + (anchor.y * -0.5 + 0.5) * box.y - xH * 0.5 -
+        textbuffer.font.lineHeight * globalScale * 0.5,
+      position.z,
+    )
+    lineWidths = splitLines(text).toSeq.mapIt(width(textbuffer.font, it, globalScale))
+    maxWidth = box.x
     aratio = getAspectRatio()
+  # echo text, anchor
 
   var
-    origin = vec3(pos.x, pos.y, position.z)
     cursorPos = origin
     lineI = 0
 
@@ -177,9 +203,9 @@
   of Left:
     cursorPos.x = origin.x
   of Center:
-    cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / 2)
+    cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5)
   of Right:
-    cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) * aratio * 2
+    cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio
 
   for i in 0 ..< text.len:
     if text[i] == Rune('\n'):
@@ -188,59 +214,66 @@
       of Left:
         cursorPos.x = origin.x
       of Center:
-        cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / 2)
+        cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5)
       of Right:
-        cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) * aratio * 2
-      cursorPos.y = cursorPos.y - glyphs.font.lineAdvance * globalScale
+        cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio
+      cursorPos.y = cursorPos.y - textbuffer.font.lineAdvance * globalScale
     else:
       if not text[i].isWhitespace():
-        glyphs.position[glyphs.cursor] = cursorPos
-        glyphs.scale[glyphs.cursor] = globalScale
-        glyphs.color[glyphs.cursor] = color
-        if text[i] in glyphs.font.descriptorGlyphIndex:
-          glyphs.glyphIndex[glyphs.cursor] = glyphs.font.descriptorGlyphIndex[text[i]]
+        textbuffer.position[textbuffer.cursor] = cursorPos
+        textbuffer.scale[textbuffer.cursor] = globalScale
+        textbuffer.color[textbuffer.cursor] = color
+        if text[i] in textbuffer.font.descriptorGlyphIndex:
+          textbuffer.glyphIndex[textbuffer.cursor] =
+            textbuffer.font.descriptorGlyphIndex[text[i]]
         else:
-          glyphs.glyphIndex[glyphs.cursor] =
-            glyphs.font.descriptorGlyphIndex[glyphs.font.fallbackCharacter]
-        inc glyphs.cursor
+          textbuffer.glyphIndex[textbuffer.cursor] =
+            textbuffer.font.descriptorGlyphIndex[textbuffer.font.fallbackCharacter]
+        inc textbuffer.cursor
 
-      if text[i] in glyphs.font.advance:
-        cursorPos.x = cursorPos.x + glyphs.font.advance[text[i]] * globalScale / aratio
+      if text[i] in textbuffer.font.advance:
+        cursorPos.x =
+          cursorPos.x + textbuffer.font.advance[text[i]] * globalScale / aratio
       else:
         cursorPos.x =
           cursorPos.x +
-          glyphs.font.advance[glyphs.font.fallbackCharacter] * globalScale / aratio
+          textbuffer.font.advance[textbuffer.font.fallbackCharacter] * globalScale /
+          aratio
 
       if i < text.len - 1:
         cursorPos.x =
           cursorPos.x +
-          glyphs.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale /
+          textbuffer.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale /
           aratio
 
 proc add*(
-    glyphs: var Glyphs,
+    textbuffer: var TextBuffer,
     text: string,
     position: Vec3f,
     alignment: TextAlignment = Left,
-    anchor: Vec2f = vec2(0, 1),
+    anchor: Vec2f = vec2(0, 0),
     scale: float32 = 1'f32,
     color: Vec4f = vec4(1, 1, 1, 1),
 ) =
-  add(glyphs, text.toRunes, position, alignment, anchor, scale, color)
+  add(textbuffer, text.toRunes, position, alignment, anchor, scale, color)
 
-proc reset*(glyphs: var Glyphs) =
-  glyphs.cursor = 0
+proc reset*(textbuffer: var TextBuffer) =
+  textbuffer.cursor = 0
+  for i in 0 ..< textbuffer.texts.len:
+    textbuffer.texts[i] = default(Text)
 
 type EMPTY = object
 const EMPTYOBJECT = EMPTY()
 
-proc renderGlyphs*(commandBuffer: VkCommandBuffer, pipeline: Pipeline, glyphs: Glyphs) =
+proc renderTextBuffer*(
+    commandBuffer: VkCommandBuffer, pipeline: Pipeline, textbuffer: TextBuffer
+) =
   renderWithPushConstant(
     commandbuffer,
     pipeline,
     EMPTYOBJECT,
-    glyphs,
+    textbuffer,
     pushConstant = TextRendering(aspectRatio: getAspectRatio()),
     fixedVertexCount = 6,
-    fixedInstanceCount = glyphs.cursor,
+    fixedInstanceCount = textbuffer.cursor,
   )
--- a/semicongine/text/font.nim	Sat Dec 21 19:32:59 2024 +0700
+++ b/semicongine/text/font.nim	Sun Dec 22 00:31:29 2024 +0700
@@ -26,7 +26,9 @@
   width, height, xoff, yoff: ptr cint,
 ): cstring {.importc, nodecl.}
 
-# proc stbtt_GetCodepointBitmapBox(info: ptr stbtt_fontinfo, codepoint: cint, scale_x, scale_y: cfloat, ix0, iy0, ix1, iy1: ptr cint) {.importc, nodecl.}
+proc stbtt_GetCodepointBox(
+  info: ptr stbtt_fontinfo, codepoint: cint, x0, y0, x1, y1: ptr cint
+): cint {.importc, nodecl.}
 
 proc stbtt_GetCodepointHMetrics(
   info: ptr stbtt_fontinfo, codepoint: cint, advance, leftBearing: ptr cint
@@ -151,10 +153,15 @@
   var ascent, descent, lineGap: cint
   stbtt_GetFontVMetrics(addr fi, addr ascent, addr descent, addr lineGap)
   result.lineAdvance = float32(ascent - descent + lineGap) * glyph2QuadScale
-  result.lineHeight = float32(ascent - descent) * glyph2QuadScale
+  result.lineHeight = float32(ascent - descent) * glyph2QuadScale # should be 1
   result.ascent = float32(ascent) * glyph2QuadScale
   result.descent = float32(descent) * glyph2QuadScale
 
+  var x0, y0, x1, y1: cint
+  discard
+    stbtt_GetCodepointBox(addr fi, cint(Rune('x')), addr x0, addr y0, addr x1, addr y1)
+  result.xHeight = float32(y1 - y0) * glyph2QuadScale
+
 proc loadFont*[N: static int](
     path: string,
     lineHeightPixels = 80'f32,
--- a/tests/test_text.nim	Sat Dec 21 19:32:59 2024 +0700
+++ b/tests/test_text.nim	Sun Dec 22 00:31:29 2024 +0700
@@ -21,9 +21,9 @@
   var pipeline = createPipeline[GlyphShader[MAX_CODEPOINTS]](
     renderPass = vulkan.swapchain.renderPass
   )
-  var glyphs = font.initGlyphs(1000, baseScale = 0.1)
+  var textbuffer = font.initTextBuffer(1000, baseScale = 0.1)
 
-  assignBuffers(renderdata, glyphs)
+  assignBuffers(renderdata, textbuffer)
   assignBuffers(renderdata, font.descriptorSet)
   uploadImages(renderdata, font.descriptorSet)
   initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet)
@@ -31,9 +31,9 @@
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     let t = getMonoTime()
-    glyphs.reset()
-    glyphs.add("Hello semicongine!", vec3(0.5, 0.5), anchor = vec2(0.5, 0.5))
-    glyphs.updateAllGPUBuffers(flush = true)
+    textbuffer.reset()
+    textbuffer.add("Hello semicongine!", vec3(0.5, 0.5), anchor = vec2(0.5, 0.5))
+    textbuffer.updateAllGPUBuffers(flush = true)
 
     withNextFrame(framebuffer, commandbuffer):
       bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline)
@@ -46,7 +46,7 @@
         vec4(0, 0, 0, 0),
       ):
         withPipeline(commandbuffer, pipeline):
-          renderGlyphs(commandbuffer, pipeline, glyphs)
+          renderTextBuffer(commandbuffer, pipeline, textbuffer)
 
   # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
@@ -73,13 +73,13 @@
   initDescriptorSet(renderdata, pipeline.layout(0), font2.descriptorSet)
   initDescriptorSet(renderdata, pipeline.layout(0), font3.descriptorSet)
 
-  var glyphs1 = font1.initGlyphs(10, baseScale = 0.1)
-  var glyphs2 = font2.initGlyphs(10, baseScale = 0.1)
-  var glyphs3 = font3.initGlyphs(10, baseScale = 0.1)
+  var textbuffer1 = font1.initTextBuffer(10, baseScale = 0.1)
+  var textbuffer2 = font2.initTextBuffer(10, baseScale = 0.1)
+  var textbuffer3 = font3.initTextBuffer(10, baseScale = 0.1)
 
-  assignBuffers(renderdata, glyphs1)
-  assignBuffers(renderdata, glyphs2)
-  assignBuffers(renderdata, glyphs3)
+  assignBuffers(renderdata, textbuffer1)
+  assignBuffers(renderdata, textbuffer2)
+  assignBuffers(renderdata, textbuffer3)
 
   var labels = ["  0", "  1", "  2"]
 
@@ -87,17 +87,17 @@
   var p = 0
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time
-    glyphs1.reset()
-    glyphs2.reset()
-    glyphs3.reset()
+    textbuffer1.reset()
+    textbuffer2.reset()
+    textbuffer3.reset()
 
-    glyphs1.add($(p + 0), vec3(0.3, 0.5))
-    glyphs2.add($(p + 1), vec3(0.5, 0.5))
-    glyphs3.add($(p + 2), vec3(0.7, 0.5))
+    textbuffer1.add($(p + 0), vec3(0.3, 0.5))
+    textbuffer2.add($(p + 1), vec3(0.5, 0.5))
+    textbuffer3.add($(p + 2), vec3(0.7, 0.5))
 
-    glyphs1.updateAllGPUBuffers(flush = true)
-    glyphs2.updateAllGPUBuffers(flush = true)
-    glyphs3.updateAllGPUBuffers(flush = true)
+    textbuffer1.updateAllGPUBuffers(flush = true)
+    textbuffer2.updateAllGPUBuffers(flush = true)
+    textbuffer3.updateAllGPUBuffers(flush = true)
 
     inc p
     withNextFrame(framebuffer, commandbuffer):
@@ -111,11 +111,11 @@
       ):
         withPipeline(commandbuffer, pipeline):
           bindDescriptorSet(commandbuffer, font1.descriptorSet, 0, pipeline)
-          renderGlyphs(commandbuffer, pipeline, glyphs1)
+          renderTextBuffer(commandbuffer, pipeline, textbuffer1)
           bindDescriptorSet(commandbuffer, font2.descriptorSet, 0, pipeline)
-          renderGlyphs(commandbuffer, pipeline, glyphs2)
+          renderTextBuffer(commandbuffer, pipeline, textbuffer2)
           bindDescriptorSet(commandbuffer, font3.descriptorSet, 0, pipeline)
-          renderGlyphs(commandbuffer, pipeline, glyphs3)
+          renderTextBuffer(commandbuffer, pipeline, textbuffer3)
 
       # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
@@ -134,30 +134,31 @@
   uploadImages(renderdata, font.descriptorSet)
   initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet)
 
-  var glyphs = font.initGlyphs(1000, baseScale = 0.1)
-  assignBuffers(renderdata, glyphs)
+  var textbuffer = font.initTextBuffer(1000, baseScale = 0.1)
+  assignBuffers(renderdata, textbuffer)
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time
 
-    glyphs.reset()
-    glyphs.add("Anchor Center", vec3(0, 0), anchor = vec2(0, 0))
-    glyphs.add("Anchor top left", vec3(0, 0), anchor = vec2(-1, 1))
-    glyphs.add("Anchor top right", vec3(0, 0), anchor = vec2(1, 1))
-    glyphs.add("Anchor bottom left", vec3(0, 0), anchor = vec2(-1, -1))
-    glyphs.add("Anchor bottom right", vec3(0, 0), anchor = vec2(1, -1))
+    textbuffer.reset()
+    textbuffer.add("Anchor at center", vec3(0, 0), anchor = vec2(0, 0))
+    textbuffer.add("Anchor at top left`", vec3(-1, 1), anchor = vec2(-1, 1))
+    textbuffer.add("Anchor at top right", vec3(1, 1), anchor = vec2(1, 1))
+    textbuffer.add("Anchor at bottom left", vec3(-1, -1), anchor = vec2(-1, -1))
+    textbuffer.add("Anchor at bottom right", vec3(1, -1), anchor = vec2(1, -1))
 
-    glyphs.add(
-      """Paragraph
-  This is a somewhat longer paragraph with a few newlines and a maximum width of 0.2.
+    textbuffer.add(
+      "Mutiline text\nLeft aligned\nCool!", vec3(-0.5, -0.5), alignment = Left
+    )
+    textbuffer.add(
+      "Mutiline text\nCenter aligned\nCool!!", vec3(0, -0.5), alignment = Center
+    )
+    textbuffer.add(
+      "Mutiline text\nRight aligned\nCool!!!", vec3(0.5, -0.5), alignment = Right
+    )
 
-  It should display with some space above and have a pleasing appearance overall! :)""",
-      vec3(0.5, 0.5),
-      anchor = vec2(0, 0),
-      alignment = Center,
-    )
-    glyphs.updateAllGPUBuffers(flush = true)
+    textbuffer.updateAllGPUBuffers(flush = true)
 
     withNextFrame(framebuffer, commandbuffer):
       bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline)
@@ -170,14 +171,13 @@
         vec4(0, 0, 0, 0),
       ):
         withPipeline(commandbuffer, pipeline):
-          renderGlyphs(commandbuffer, pipeline, glyphs)
+          renderTextBuffer(commandbuffer, pipeline, textbuffer)
 
       # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
   destroyPipeline(pipeline)
   destroyRenderData(renderdata)
 
-#[
 proc test_04_lots_of_texts(time: float32) =
   var font = loadFont[MAX_CODEPOINTS]("DejaVuSans.ttf", lineHeightPixels = 160)
   var renderdata = initRenderData()
@@ -186,9 +186,12 @@
     renderPass = vulkan.swapchain.renderPass
   )
 
-  var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas.copy()))
-  uploadImages(renderdata, ds)
-  initDescriptorSet(renderdata, pipeline.layout(0), ds)
+  assignBuffers(renderdata, font.descriptorSet)
+  uploadImages(renderdata, font.descriptorSet)
+  initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet)
+
+  var textbuffer = font.initTextBuffer(1000, baseScale = 0.1)
+  assignBuffers(renderdata, textbuffer)
 
   var labels: seq[Textbox]
   var positions = newSeq[Vec3f](100)
@@ -203,10 +206,9 @@
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
-    for l in labels.mitems:
-      l.refresh()
+    textbuffer.reset()
     withNextFrame(framebuffer, commandbuffer):
-      bindDescriptorSet(commandbuffer, ds, 0, pipeline)
+      bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline)
       withRenderPass(
         vulkan.swapchain.renderPass,
         framebuffer,
@@ -216,16 +218,12 @@
         vec4(0, 0, 0, 0),
       ):
         withPipeline(commandbuffer, pipeline):
-          for i in 0 ..< labels.len:
-            render(
-              commandbuffer, pipeline, labels[i], positions[i], colors[i], scales[i]
-            )
+          renderTextBuffer(commandbuffer, pipeline, textbuffer)
 
         # cleanup
   checkVkResult vkDeviceWaitIdle(vulkan.device)
   destroyPipeline(pipeline)
   destroyRenderData(renderdata)
-]#
 
 when isMainModule:
   var time = 100'f32