changeset 1411:5273bb68cf85

fix: bug with occupation of whitespace glyphs, performance improvment, generational text-handles
author sam <sam@basx.dev>
date Thu, 26 Dec 2024 16:06:22 +0700
parents 99d5b42cf32d
children e10b230c70bc
files semicongine/text.nim semicongine/text/font.nim
diffstat 2 files changed, 59 insertions(+), 42 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/text.nim	Thu Dec 26 11:20:47 2024 +0700
+++ b/semicongine/text.nim	Thu Dec 26 16:06:22 2024 +0700
@@ -75,11 +75,14 @@
     xHeight*: float32 # from baseline to height of lowercase x
     descriptorSet*: DescriptorSetData[GlyphDescriptorSet[MaxGlyphs]]
     descriptorGlyphIndex: Table[Rune, uint16]
+    descriptorGlyphIndexRev*: Table[uint16, Rune] # only used for debugging atm
     fallbackCharacter: Rune
 
   Font*[MaxGlyphs: static int] = ref FontObj[MaxGlyphs]
 
-  TextHandle* = distinct int
+  TextHandle* = object
+    index: uint32
+    generation: uint32
 
   TextAlignment* = enum
     Left
@@ -98,6 +101,7 @@
 
   TextBuffer*[MaxGlyphs: static int] = object
     cursor: int
+    generation: uint32
     font*: Font[MaxGlyphs]
     baseScale*: float32
     position*: GPUArray[Vec3f, VertexBufferMapped]
@@ -129,7 +133,6 @@
   result.scale.data.setLen(bufferSize)
   result.color.data.setLen(bufferSize)
   result.glyphIndex.data.setLen(bufferSize)
-  result.texts.setLen(bufferSize) # waste a lot of memory?
   assignBuffers(renderdata, result)
 
 iterator splitLines(text: seq[Rune]): seq[Rune] =
@@ -160,22 +163,20 @@
   let nLines = text.countIt(it == Rune('\n')).float32
   let h = (nLines * font.lineAdvance + font.lineHeight)
   let w = max(splitLines(text).toSeq.mapIt(width(font, it)))
+  return vec2(w, h)
 
-  return vec2(w, h)
+proc textDimension*(font: Font, text: string): Vec2f =
+  textDimension(font, text.toRunes())
 
 proc updateGlyphData*(textbuffer: var TextBuffer, textHandle: TextHandle) =
+  assert textHandle.generation == textbuffer.generation
   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
+    textI = textHandle.index
+    text = textbuffer.texts[textI].text
+    position = textbuffer.texts[textI].position
+    anchor = textbuffer.texts[textI].anchor
 
-    globalScale = scale * textbuffer.baseScale
+    globalScale = textbuffer.texts[textI].scale * textbuffer.baseScale
     box = textDimension(textbuffer.font, text) * globalScale
     xH = textbuffer.font.xHeight * globalScale
     aratio = getAspectRatio()
@@ -192,7 +193,7 @@
     cursorPos = origin
     lineI = 0
 
-  case alignment
+  case textbuffer.texts[textI].alignment
   of Left:
     cursorPos.x = origin.x
   of Center:
@@ -200,11 +201,14 @@
   of Right:
     cursorPos.x = origin.x + (maxWidth - lineWidths[lineI]) / aratio
 
-  for i in 0 ..< capacity:
+  var bufferOffset = textbuffer.texts[textI].bufferOffset
+  for i in 0 ..< textbuffer.texts[textI].capacity:
+    assert bufferOffset <
+      textbuffer.texts[textI].bufferOffset + textbuffer.texts[textI].capacity
     if i < text.len:
       if text[i] == Rune('\n'):
         inc lineI
-        case alignment
+        case textbuffer.texts[textI].alignment
         of Left:
           cursorPos.x = origin.x
         of Center:
@@ -214,15 +218,17 @@
         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
+          textbuffer.position[bufferOffset] = cursorPos
+          textbuffer.scale[bufferOffset] = globalScale
+          textbuffer.color[bufferOffset] = textbuffer.texts[textI].color
           if text[i] in textbuffer.font.descriptorGlyphIndex:
-            textbuffer.glyphIndex[offset + i] =
+            textbuffer.glyphIndex[bufferOffset] =
               textbuffer.font.descriptorGlyphIndex[text[i]]
           else:
-            textbuffer.glyphIndex[offset + i] =
+            textbuffer.glyphIndex[bufferOffset] =
               textbuffer.font.descriptorGlyphIndex[textbuffer.font.fallbackCharacter]
+          # only use up buffer space when we actually draw a glyph i.e. whitespace is not using buffer space
+          inc bufferOffset
 
         if text[i] in textbuffer.font.advance:
           cursorPos.x =
@@ -239,14 +245,22 @@
             textbuffer.font.kerning.getOrDefault((text[i], text[i + 1]), 0) * globalScale /
             aratio
     else:
-      textbuffer.position[offset + i] = vec3()
-      textbuffer.scale[offset + i] = 0
-      textbuffer.color[offset + i] = vec4()
-      textbuffer.glyphIndex[offset + i] = 0
+      textbuffer.position[bufferOffset] = vec3()
+      textbuffer.scale[bufferOffset] = 0
+      textbuffer.color[bufferOffset] = vec4()
+      textbuffer.glyphIndex[bufferOffset] = 0
+      inc bufferOffset
 
 proc updateGlyphData*(textbuffer: var TextBuffer) =
   for i in 0 ..< textbuffer.texts.len:
-    textbuffer.updateGlyphData(TextHandle(i))
+    textbuffer.updateGlyphData(
+      TextHandle(index: uint32(i), generation: textbuffer.generation)
+    )
+
+proc reset*(textbuffer: var TextBuffer) =
+  inc textbuffer.generation # integer overflow *should* be okay here
+  textbuffer.cursor = 0
+  textbuffer.texts.setLen(0)
 
 proc refresh*(textbuffer: var TextBuffer) =
   textbuffer.updateGlyphData()
@@ -268,7 +282,8 @@
   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)
+  result =
+    TextHandle(generation: textbuffer.generation, index: textbuffer.texts.len.uint32)
 
   textbuffer.texts.add Text(
     bufferOffset: textbuffer.cursor,
@@ -281,7 +296,6 @@
     capacity: cap,
   )
   textbuffer.cursor += cap
-  textbuffer.updateGlyphData(result)
 
 proc add*(
     textbuffer: var TextBuffer,
@@ -296,36 +310,38 @@
   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
+  assert textHandle.generation == textbuffer.generation
+  if text.len <= textbuffer.texts[textHandle.index].capacity:
+    textbuffer.texts[textHandle.index].text = text
   else:
-    textbuffer.texts[int(textHandle)].text =
-      text[0 ..< textbuffer.texts[int(textHandle)].capacity]
+    textbuffer.texts[textHandle.index].text =
+      text[0 ..< textbuffer.texts[textHandle.index].capacity]
 
 proc text*(textbuffer: var TextBuffer, textHandle: TextHandle, text: string) =
+  assert textHandle.generation == textbuffer.generation
   text(textbuffer, textHandle, text.toRunes)
 
 proc position*(textbuffer: var TextBuffer, textHandle: TextHandle, position: Vec3f) =
-  textbuffer.texts[int(textHandle)].position = position
+  assert textHandle.generation == textbuffer.generation
+  textbuffer.texts[textHandle.index].position = position
 
 proc alignment*(
     textbuffer: var TextBuffer, textHandle: TextHandle, alignment: TextAlignment
 ) =
-  textbuffer.texts[int(textHandle)].alignment = alignment
+  assert textHandle.generation == textbuffer.generation
+  textbuffer.texts[textHandle.index].alignment = alignment
 
 proc anchor*(textbuffer: var TextBuffer, textHandle: TextHandle, anchor: Vec2f) =
-  textbuffer.texts[int(textHandle)].anchor = anchor
+  assert textHandle.generation == textbuffer.generation
+  textbuffer.texts[textHandle.index].anchor = anchor
 
 proc scale*(textbuffer: var TextBuffer, textHandle: TextHandle, scale: float32) =
-  textbuffer.texts[int(textHandle)].scale = scale
+  assert textHandle.generation == textbuffer.generation
+  textbuffer.texts[textHandle.index].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
-  for i in 0 ..< textbuffer.texts.len:
-    textbuffer.texts[i] = default(Text)
+  assert textHandle.generation == textbuffer.generation
+  textbuffer.texts[textHandle.index].color = color
 
 type EMPTY = object
 const EMPTYOBJECT = EMPTY()
--- a/semicongine/text/font.nim	Thu Dec 26 11:20:47 2024 +0700
+++ b/semicongine/text/font.nim	Thu Dec 26 16:06:22 2024 +0700
@@ -141,6 +141,7 @@
     if i == 0:
       result.fallbackCharacter = codePoint
     result.descriptorGlyphIndex[codePoint] = i.uint16
+    result.descriptorGlyphIndexRev[i.uint16] = codePoint # only used for debugging atm
 
     # kerning
     for codePointAfter in codePoints: