changeset 1408:17d960ff6a24

did: implement decent text rendering (I hope, we'll see)
author sam <sam@basx.dev>
date Sun, 22 Dec 2024 22:32:12 +0700
parents 56f927b89716
children 5a56f8ac328b
files semicongine/text.nim tests/test_text.nim
diffstat 2 files changed, 178 insertions(+), 114 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/text.nim	Sun Dec 22 00:31:29 2024 +0700
+++ b/semicongine/text.nim	Sun Dec 22 22:32:12 2024 +0700
@@ -47,6 +47,8 @@
     fallbackCharacter: Rune
 
   Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs]
+
+  TextHandle* = distinct int
   Text = object
     bufferOffset: int
     text: seq[Rune]
@@ -55,6 +57,7 @@
     anchor: Vec2f = vec2()
     scale: float32 = 0
     color: Vec4f = vec4(1, 1, 1, 1)
+    capacity: int
 
   TextBuffer*[MaxGlyphs: static int] = object
     cursor: int
@@ -156,44 +159,30 @@
 
   return vec2(w, h)
 
-proc add*(
-    textbuffer: var TextBuffer,
-    text: seq[Rune],
-    position: Vec3f,
-    alignment: TextAlignment = Left,
-    anchor: Vec2f = vec2(0, 0),
-    scale: float32 = 1'f32,
-    color: Vec4f = vec4(1, 1, 1, 1),
-) =
-  ## This should be called again after aspect ratio of window changes 
+proc updateGlyphData*(textbuffer: var TextBuffer, textHandle: TextHandle) =
+  let
+    i = int(textHandle)
+    text = textbuffer.texts[i].text
+    position = textbuffer.texts[i].position
+    alignment = textbuffer.texts[i].alignment
+    anchor = textbuffer.texts[i].anchor
+    scale = textbuffer.texts[i].scale
+    color = textbuffer.texts[i].color
+    offset = textbuffer.texts[i].bufferOffset
+    capacity = textbuffer.texts[i].capacity
 
-  assert text.len <= textbuffer.position.len,
-    &"Set {text.len} but TextBuffer-object only supports {textbuffer.position.len}"
-
-  textbuffer.texts.add Text(
-    bufferOffset: textbuffer.cursor,
-    text: text,
-    position: position,
-    alignment: alignment,
-    anchor: anchor,
-    scale: scale,
-    color: color,
-  )
-
-  let
     globalScale = scale * textbuffer.baseScale
     box = textDimension(textbuffer.font, text, globalScale)
     xH = textbuffer.font.xHeight * globalScale
+    aratio = getAspectRatio()
     origin = vec3(
-      position.x - (anchor.x * 0.5 + 0.5) * box.x / getAspectRatio(),
+      position.x - (anchor.x * 0.5 + 0.5) * box.x / aratio,
       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
     cursorPos = origin
@@ -207,44 +196,88 @@
   of Right:
     cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio
 
-  for i in 0 ..< text.len:
-    if text[i] == Rune('\n'):
-      inc lineI
-      case alignment
-      of Left:
-        cursorPos.x = origin.x
-      of Center:
-        cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5)
-      of Right:
-        cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio
-      cursorPos.y = cursorPos.y - textbuffer.font.lineAdvance * globalScale
+  for i in 0 ..< capacity:
+    if i < text.len:
+      if text[i] == Rune('\n'):
+        inc lineI
+        case alignment
+        of Left:
+          cursorPos.x = origin.x
+        of Center:
+          cursorPos.x = origin.x + ((maxWidth - lineWidths[lineI]) / aratio * 0.5)
+        of Right:
+          cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio
+        cursorPos.y = cursorPos.y - textbuffer.font.lineAdvance * globalScale
+      else:
+        if not text[i].isWhitespace():
+          textbuffer.position[offset + i] = cursorPos
+          textbuffer.scale[offset + i] = globalScale
+          textbuffer.color[offset + i] = color
+          if text[i] in textbuffer.font.descriptorGlyphIndex:
+            textbuffer.glyphIndex[offset + i] =
+              textbuffer.font.descriptorGlyphIndex[text[i]]
+          else:
+            textbuffer.glyphIndex[offset + i] =
+              textbuffer.font.descriptorGlyphIndex[textbuffer.font.fallbackCharacter]
+
+        if text[i] in textbuffer.font.advance:
+          cursorPos.x =
+            cursorPos.x + textbuffer.font.advance[text[i]] * globalScale / aratio
+        else:
+          cursorPos.x =
+            cursorPos.x +
+            textbuffer.font.advance[textbuffer.font.fallbackCharacter] * globalScale /
+            aratio
+
+        if i < text.len - 1:
+          cursorPos.x =
+            cursorPos.x +
+            textbuffer.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale /
+            aratio
     else:
-      if not text[i].isWhitespace():
-        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:
-          textbuffer.glyphIndex[textbuffer.cursor] =
-            textbuffer.font.descriptorGlyphIndex[textbuffer.font.fallbackCharacter]
-        inc textbuffer.cursor
+      textbuffer.position[offset + i] = vec3()
+      textbuffer.scale[offset + i] = 0
+      textbuffer.color[offset + i] = vec4()
+      textbuffer.glyphIndex[offset + i] = 0
+
+proc updateGlyphData*(textbuffer: var TextBuffer) =
+  for i in 0 ..< textbuffer.texts.len:
+    textbuffer.updateGlyphData(TextHandle(i))
+
+proc refresh*(textbuffer: var TextBuffer) =
+  textbuffer.updateGlyphData()
+  textbuffer.updateAllGPUBuffers(flush = true)
 
-      if text[i] in textbuffer.font.advance:
-        cursorPos.x =
-          cursorPos.x + textbuffer.font.advance[text[i]] * globalScale / aratio
-      else:
-        cursorPos.x =
-          cursorPos.x +
-          textbuffer.font.advance[textbuffer.font.fallbackCharacter] * globalScale /
-          aratio
+proc add*(
+    textbuffer: var TextBuffer,
+    text: seq[Rune],
+    position: Vec3f,
+    alignment: TextAlignment = Left,
+    anchor: Vec2f = vec2(0, 0),
+    scale: float32 = 1'f32,
+    color: Vec4f = vec4(1, 1, 1, 1),
+    capacity: int = 0,
+): TextHandle =
+  ## This should be called again after aspect ratio of window changes 
 
-      if i < text.len - 1:
-        cursorPos.x =
-          cursorPos.x +
-          textbuffer.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale /
-          aratio
+  let cap = if capacity == 0: text.len else: capacity
+  assert textbuffer.cursor + cap <= textbuffer.position.len,
+    &"Text is too big for TextBuffer ({textbuffer.position.len - textbuffer.cursor} left, but need {cap})"
+
+  result = TextHandle(textbuffer.texts.len)
+
+  textbuffer.texts.add Text(
+    bufferOffset: textbuffer.cursor,
+    text: text,
+    position: position,
+    alignment: alignment,
+    anchor: anchor,
+    scale: scale,
+    color: color,
+    capacity: cap,
+  )
+  textbuffer.cursor += cap
+  textbuffer.updateGlyphData(result)
 
 proc add*(
     textbuffer: var TextBuffer,
@@ -254,8 +287,36 @@
     anchor: Vec2f = vec2(0, 0),
     scale: float32 = 1'f32,
     color: Vec4f = vec4(1, 1, 1, 1),
+    capacity: int = 0,
+): TextHandle =
+  add(textbuffer, text.toRunes, position, alignment, anchor, scale, color, capacity)
+
+proc text*(textbuffer: var TextBuffer, textHandle: TextHandle, text: seq[Rune]) =
+  if text.len <= textbuffer.texts[int(textHandle)].capacity:
+    textbuffer.texts[int(textHandle)].text = text
+  else:
+    textbuffer.texts[int(textHandle)].text =
+      text[0 ..< textbuffer.texts[int(textHandle)].capacity]
+
+proc text*(textbuffer: var TextBuffer, textHandle: TextHandle, text: string) =
+  text(textbuffer, textHandle, text.toRunes)
+
+proc position*(textbuffer: var TextBuffer, textHandle: TextHandle, position: Vec3f) =
+  textbuffer.texts[int(textHandle)].position = position
+
+proc alignment*(
+    textbuffer: var TextBuffer, textHandle: TextHandle, alignment: TextAlignment
 ) =
-  add(textbuffer, text.toRunes, position, alignment, anchor, scale, color)
+  textbuffer.texts[int(textHandle)].alignment = alignment
+
+proc anchor*(textbuffer: var TextBuffer, textHandle: TextHandle, anchor: Vec2f) =
+  textbuffer.texts[int(textHandle)].anchor = anchor
+
+proc scale*(textbuffer: var TextBuffer, textHandle: TextHandle, scale: float32) =
+  textbuffer.texts[int(textHandle)].scale = scale
+
+proc color*(textbuffer: var TextBuffer, textHandle: TextHandle, color: Vec4f) =
+  textbuffer.texts[int(textHandle)].color = color
 
 proc reset*(textbuffer: var TextBuffer) =
   textbuffer.cursor = 0
--- a/tests/test_text.nim	Sun Dec 22 00:31:29 2024 +0700
+++ b/tests/test_text.nim	Sun Dec 22 22:32:12 2024 +0700
@@ -28,12 +28,13 @@
   uploadImages(renderdata, font.descriptorSet)
   initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet)
 
+  discard textbuffer.add("Hello semicongine!", vec3())
+
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     let t = getMonoTime()
-    textbuffer.reset()
-    textbuffer.add("Hello semicongine!", vec3(0.5, 0.5), anchor = vec2(0.5, 0.5))
-    textbuffer.updateAllGPUBuffers(flush = true)
+    if windowWasResized():
+      textbuffer.refresh()
 
     withNextFrame(framebuffer, commandbuffer):
       bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline)
@@ -81,25 +82,25 @@
   assignBuffers(renderdata, textbuffer2)
   assignBuffers(renderdata, textbuffer3)
 
-  var labels = ["  0", "  1", "  2"]
+  var p = 0
+  let l1 = textbuffer1.add($(p + 0), vec3(0.3, 0.5), capacity = 5)
+  let l2 = textbuffer2.add($(p + 1), vec3(0.5, 0.5), capacity = 5)
+  let l3 = textbuffer3.add($(p + 2), vec3(0.7, 0.5), capacity = 5)
 
   var start = getMonoTime()
-  var p = 0
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time
-    textbuffer1.reset()
-    textbuffer2.reset()
-    textbuffer3.reset()
-
-    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))
-
-    textbuffer1.updateAllGPUBuffers(flush = true)
-    textbuffer2.updateAllGPUBuffers(flush = true)
-    textbuffer3.updateAllGPUBuffers(flush = true)
 
     inc p
+
+    textbuffer1.text(l1, $(p + 0))
+    textbuffer2.text(l2, $(p + 1))
+    textbuffer3.text(l3, $(p + 2))
+
+    textbuffer1.refresh()
+    textbuffer2.refresh()
+    textbuffer3.refresh()
+
     withNextFrame(framebuffer, commandbuffer):
       withRenderPass(
         vulkan.swapchain.renderPass,
@@ -137,28 +138,27 @@
   var textbuffer = font.initTextBuffer(1000, baseScale = 0.1)
   assignBuffers(renderdata, textbuffer)
 
+  discard textbuffer.add("Anchor at center", vec3(0, 0), anchor = vec2(0, 0))
+  discard textbuffer.add("Anchor at top left`", vec3(-1, 1), anchor = vec2(-1, 1))
+  discard textbuffer.add("Anchor at top right", vec3(1, 1), anchor = vec2(1, 1))
+  discard textbuffer.add("Anchor at bottom left", vec3(-1, -1), anchor = vec2(-1, -1))
+  discard textbuffer.add("Anchor at bottom right", vec3(1, -1), anchor = vec2(1, -1))
+
+  discard textbuffer.add(
+    "Mutiline text\nLeft aligned\nCool!", vec3(-0.5, -0.5), alignment = Left
+  )
+  discard textbuffer.add(
+    "Mutiline text\nCenter aligned\nCool!!", vec3(0, -0.5), alignment = Center
+  )
+  discard textbuffer.add(
+    "Mutiline text\nRight aligned\nCool!!!", vec3(0.5, -0.5), alignment = Right
+  )
+
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time
-
-    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))
-
-    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
-    )
-
-    textbuffer.updateAllGPUBuffers(flush = true)
+    if windowWasResized():
+      textbuffer.refresh()
 
     withNextFrame(framebuffer, commandbuffer):
       bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline)
@@ -190,24 +190,27 @@
   uploadImages(renderdata, font.descriptorSet)
   initDescriptorSet(renderdata, pipeline.layout(0), font.descriptorSet)
 
-  var textbuffer = font.initTextBuffer(1000, baseScale = 0.1)
+  var textbuffer = font.initTextBuffer(3000, baseScale = 0.1)
   assignBuffers(renderdata, textbuffer)
 
-  var labels: seq[Textbox]
-  var positions = newSeq[Vec3f](100)
-  var colors = newSeq[Vec4f](100)
-  var scales = newSeq[Vec2f](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] = vec2(rand(0.5'f32 .. 1.5'f32), rand(0.5'f32 .. 1.5'f32))
-    labels.add initTextbox(renderdata, pipeline.layout(0), font, 0.001, $i)
+  for i in 0 ..< 1000:
+    discard textbuffer.add(
+      $i,
+      vec3(rand(-0.8 .. 0.8), rand(-0.8 .. 0.8), rand(-0.1 .. 0.1)),
+      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.5'f32 .. 1.5'f32),
+    )
 
   var start = getMonoTime()
+  var last = start
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
-    textbuffer.reset()
+    let n = getMonoTime()
+    echo (n - last).inMicroseconds() / 1000
+    last = n
     withNextFrame(framebuffer, commandbuffer):
+      if windowWasResized():
+        textbuffer.refresh()
       bindDescriptorSet(commandbuffer, font.descriptorSet, 0, pipeline)
       withRenderPass(
         vulkan.swapchain.renderPass,
@@ -226,7 +229,7 @@
   destroyRenderData(renderdata)
 
 when isMainModule:
-  var time = 100'f32
+  var time = 1'f32
   initVulkan()
 
   for depthBuffer in [true, false]:
@@ -234,10 +237,10 @@
     setupSwapchain(renderpass = renderpass)
 
     # tests a simple triangle with minimalistic shader and vertex format
-    # test_01_static_label(time)
-    # test_02_multi_counter(time)
+    test_01_static_label(time)
+    test_02_multi_counter(time)
     test_03_layouting(time)
-    # test_04_lots_of_texts(time)
+    test_04_lots_of_texts(time)
 
     checkVkResult vkDeviceWaitIdle(vulkan.device)
     vkDestroyRenderPass(vulkan.device, renderpass.vk, nil)