changeset 1236:176383220123

add: first font-rendering test
author sam <sam@basx.dev>
date Sat, 20 Jul 2024 17:45:44 +0700
parents c70fee6568f6
children 97813ac43cfb
files semiconginev2/rendering/renderer.nim semiconginev2/resources.nim semiconginev2/text.nim semiconginev2/text/font.nim semiconginev2/text/textbox.nim tests/resources/default/Overhaul.ttf tests/resources/default/donut.glb tests/resources/default/key.ogg tests/resources/default/test1.glb tests/test_text.nim
diffstat 10 files changed, 207 insertions(+), 127 deletions(-) [+]
line wrap: on
line diff
--- a/semiconginev2/rendering/renderer.nim	Sat Jul 20 15:45:02 2024 +0700
+++ b/semiconginev2/rendering/renderer.nim	Sat Jul 20 17:45:44 2024 +0700
@@ -290,14 +290,14 @@
     WithStagingBuffer((gpuData.buffer.vk, gpuData.offset), gpuData.size, stagingPtr):
       copyMem(stagingPtr, gpuData.rawPointer, gpuData.size)
 
-proc UpdateAllGPUBuffers*[T](value: T) =
+proc UpdateAllGPUBuffers*[T](value: T, flush = false) =
   for name, fieldvalue in value.fieldPairs():
     when typeof(fieldvalue) is GPUData:
-      UpdateGPUBuffer(fieldvalue)
+      UpdateGPUBuffer(fieldvalue, flush = flush)
     when typeof(fieldvalue) is array:
       when elementType(fieldvalue) is GPUData:
         for entry in fieldvalue:
-          UpdateGPUBuffer(entry)
+          UpdateGPUBuffer(entry, flush = flush)
 
 proc AssignGPUData(renderdata: var RenderData, value: var GPUData) =
   # find buffer that has space
--- a/semiconginev2/resources.nim	Sat Jul 20 15:45:02 2024 +0700
+++ b/semiconginev2/resources.nim	Sat Jul 20 17:45:44 2024 +0700
@@ -156,19 +156,6 @@
 proc LoadConfig*(path: string, package = DEFAULT_PACKAGE): Config =
   path.loadResource_intern(package = package).loadConfig(filename = path)
 
-proc LoadFont*(
-  path: string,
-  name = "",
-  lineHeightPixels = 80'f32,
-  additional_codepoints: openArray[Rune] = [],
-  charset = ASCII_CHARSET,
-  package = DEFAULT_PACKAGE
-): Font =
-  var thename = name
-  if thename == "":
-    thename = path.splitFile().name
-  loadResource_intern(path, package = package).ReadTrueType(name, charset & additional_codepoints.toSeq, lineHeightPixels)
-
 proc LoadMeshes*(path: string, defaultMaterial: MaterialType, package = DEFAULT_PACKAGE): seq[MeshTree] =
   loadResource_intern(path, package = package).ReadglTF(defaultMaterial)
 
--- a/semiconginev2/text.nim	Sat Jul 20 15:45:02 2024 +0700
+++ b/semiconginev2/text.nim	Sat Jul 20 17:45:44 2024 +0700
@@ -26,6 +26,7 @@
     color: Vec4f
     position: Vec3f
     scale: float32
+    aspectratio: float32
   TextboxDescriptorSet = object
     textbox: GPUValue[TextboxData, UniformBufferMapped]
     fontAtlas: Image[Gray]
@@ -36,11 +37,13 @@
     fragmentUv {.Pass.}: Vec2f
     color {.ShaderOutput.}: Vec4f
     descriptorSets {.DescriptorSets.}: (TextboxDescriptorSet, )
-    vertexCode = &"""
-  gl_Position = vec4(position * textbox.scale + textbox.position, 1.0);
+    vertexCode = """void main() {
+  gl_Position = vec4(position * vec3(1, textbox.aspectratio, 1) * textbox.scale + textbox.position, 1.0);
   fragmentUv = uv;
-  """
-    fragmentCode = &"""color = vec4(textbox.color.rgb, textbox.color.rgb.a * texture(fontAtlas, fragmentUv).r);"""
+}  """
+    fragmentCode = """void main() {
+    color = vec4(textbox.color.rgb, textbox.color.a * texture(fontAtlas, fragmentUv).r);
+}"""
 
 
 include ./text/font
--- a/semiconginev2/text/font.nim	Sat Jul 20 15:45:02 2024 +0700
+++ b/semiconginev2/text/font.nim	Sat Jul 20 17:45:44 2024 +0700
@@ -20,11 +20,13 @@
   var
     indata = stream.readAll()
     fontinfo: stbtt_fontinfo
-  if stbtt_InitFont(addr fontinfo, addr indata[0], 0) == 0:
-    raise newException(Exception, "An error occured while loading PNG file")
+  if stbtt_InitFont(addr fontinfo, indata.ToCPointer, 0) == 0:
+    raise newException(Exception, "An error occured while loading font file")
 
-  result.name = name
-  result.fontscale = float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(lineHeightPixels)))
+  result = Font(
+    name: name,
+    fontscale: float32(stbtt_ScaleForPixelHeight(addr fontinfo, cfloat(lineHeightPixels))),
+  )
 
   var ascent, descent, lineGap: cint
   stbtt_GetFontVMetrics(addr fontinfo, addr ascent, addr descent, addr lineGap)
@@ -109,6 +111,19 @@
         cint(codePointAfter)
       )) * result.fontscale
 
+proc LoadFont*(
+  path: string,
+  name = "",
+  lineHeightPixels = 80'f32,
+  additional_codepoints: openArray[Rune] = [],
+  charset = ASCII_CHARSET,
+  package = DEFAULT_PACKAGE
+): Font =
+  var thename = name
+  if thename == "":
+    thename = path.splitFile().name
+  loadResource_intern(path, package = package).ReadTrueType(thename, charset & additional_codepoints.toSeq, lineHeightPixels)
+
 func TextWidth*(text: seq[Rune], font: FontObj): float32 =
   var currentWidth = 0'f32
   var lineWidths: seq[float32]
--- a/semiconginev2/text/textbox.nim	Sat Jul 20 15:45:02 2024 +0700
+++ b/semiconginev2/text/textbox.nim	Sat Jul 20 17:45:44 2024 +0700
@@ -14,165 +14,177 @@
     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: DescriptorSet[TextboxDescriptorSet]
-
-func `$`*(text: Textbox): string =
-  "\"" & $text.text[0 ..< min(text.text.len, 16)] & "\""
+    position*: GPUArray[Vec3f, VertexBuffer]
+    uv*: GPUArray[Vec2f, VertexBuffer]
+    indices*: GPUArray[uint16, IndexBuffer]
+    shaderdata*: DescriptorSet[TextboxDescriptorSet]
 
-proc RefreshShaderdata(text: Textbox) =
-  if not text.dirtyShaderdata:
-    return
-  text.shaderdata.data.textbox.UpdateGPUBuffer()
+func `$`*(textbox: Textbox): string =
+  "\"" & $textbox.text[0 ..< min(textbox.text.len, 16)] & "\""
 
-proc RefreshGeometry(text: var Textbox) =
-  if not text.dirtyGeometry and text.processedText == text.lastRenderedText:
-    return
+proc RefreshShaderdata(textbox: Textbox) =
+  textbox.shaderdata.data.textbox.UpdateGPUBuffer()
 
+proc RefreshGeometry(textbox: var Textbox) =
   # pre-calculate text-width
   var width = 0'f32
   var lineWidths: seq[float32]
-  for i in 0 ..< text.processedText.len:
-    if text.processedText[i] == NEWLINE:
+  for i in 0 ..< textbox.processedText.len:
+    if textbox.processedText[i] == NEWLINE:
       lineWidths.add width
       width = 0'f32
     else:
-      if not (i == text.processedText.len - 1 and text.processedText[i].isWhiteSpace):
-        width += text.font.glyphs[text.processedText[i]].advance
-      if i < text.processedText.len - 1:
-        width += text.font.kerning[(text.processedText[i], text.processedText[i + 1])]
+      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])]
   lineWidths.add width
-  var height = float32(lineWidths.len - 1) * text.font.lineAdvance + text.font.capHeight
+  var height = float32(lineWidths.len - 1) * textbox.font.lineAdvance + textbox.font.capHeight
   if lineWidths[^1] == 0 and lineWidths.len > 1:
     height -= 1
 
-  let anchorY = (case text.verticalAlignment
+  let anchorY = (case textbox.verticalAlignment
     of Top: 0'f32
     of Center: height / 2
-    of Bottom: height) - text.font.capHeight
+    of Bottom: height) - textbox.font.capHeight
 
   var
     offsetX = 0'f32
     offsetY = 0'f32
     lineIndex = 0
-    anchorX = case text.horizontalAlignment
+    anchorX = case textbox.horizontalAlignment
       of Left: 0'f32
       of Center: lineWidths[lineIndex] / 2
       of Right: lineWidths[lineIndex]
-  for i in 0 ..< text.maxLen:
+  for i in 0 ..< textbox.maxLen:
     let vertexOffset = i * 4
-    if i < text.processedText.len:
-      if text.processedText[i] == Rune('\n'):
+    if i < textbox.processedText.len:
+      if textbox.processedText[i] == Rune('\n'):
         offsetX = 0
-        offsetY += text.font.lineAdvance
-        text.position.data[vertexOffset + 0] = NewVec3f()
-        text.position.data[vertexOffset + 1] = NewVec3f()
-        text.position.data[vertexOffset + 2] = NewVec3f()
-        text.position.data[vertexOffset + 3] = NewVec3f()
+        offsetY += textbox.font.lineAdvance
+        textbox.position.data[vertexOffset + 0] = NewVec3f()
+        textbox.position.data[vertexOffset + 1] = NewVec3f()
+        textbox.position.data[vertexOffset + 2] = NewVec3f()
+        textbox.position.data[vertexOffset + 3] = NewVec3f()
         inc lineIndex
-        anchorX = case text.horizontalAlignment
+        anchorX = case textbox.horizontalAlignment
           of Left: 0'f32
           of Center: lineWidths[lineIndex] / 2
           of Right: lineWidths[lineIndex]
       else:
         let
-          glyph = text.font.glyphs[text.processedText[i]]
+          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
 
-        text.position.data[vertexOffset + 0] = NewVec3f(left - anchorX, bottom - anchorY)
-        text.position.data[vertexOffset + 1] = NewVec3f(left - anchorX, top - anchorY)
-        text.position.data[vertexOffset + 2] = NewVec3f(right - anchorX, top - anchorY)
-        text.position.data[vertexOffset + 3] = NewVec3f(right - anchorX, bottom - anchorY)
+        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)
 
-        text.uv.data[vertexOffset + 0] = glyph.uvs[0]
-        text.uv.data[vertexOffset + 1] = glyph.uvs[1]
-        text.uv.data[vertexOffset + 2] = glyph.uvs[2]
-        text.uv.data[vertexOffset + 3] = glyph.uvs[3]
+        textbox.uv.data[vertexOffset + 0] = glyph.uvs[0]
+        textbox.uv.data[vertexOffset + 1] = glyph.uvs[1]
+        textbox.uv.data[vertexOffset + 2] = glyph.uvs[2]
+        textbox.uv.data[vertexOffset + 3] = glyph.uvs[3]
 
         offsetX += glyph.advance
-        if i < text.processedText.len - 1:
-          offsetX += text.font.kerning[(text.processedText[i], text.processedText[i + 1])]
+        if i < textbox.processedText.len - 1:
+          offsetX += textbox.font.kerning[(textbox.processedText[i], textbox.processedText[i + 1])]
     else:
-      text.position.data[vertexOffset + 0] = NewVec3f()
-      text.position.data[vertexOffset + 1] = NewVec3f()
-      text.position.data[vertexOffset + 2] = NewVec3f()
-      text.position.data[vertexOffset + 3] = NewVec3f()
-  text.lastRenderedText = text.processedText
-  text.dirtyGeometry = false
+      textbox.position.data[vertexOffset + 0] = NewVec3f()
+      textbox.position.data[vertexOffset + 1] = NewVec3f()
+      textbox.position.data[vertexOffset + 2] = NewVec3f()
+      textbox.position.data[vertexOffset + 3] = NewVec3f()
+  textbox.lastRenderedText = textbox.processedText
+
+func text*(textbox: Textbox): seq[Rune] =
+  textbox.text
 
-proc Refresh*(textbox: var Textbox) =
-  textbox.RefreshShaderdata()
-  textbox.RefreshGeometry()
+proc `text=`*(textbox: var Textbox, newText: seq[Rune]) =
+  if newText[0 ..< min(newText.len, textbox.maxLen)] == textbox.text:
+    return
 
-func text*(text: Textbox): seq[Rune] =
-  text.text
+  textbox.text = newText[0 ..< min(newText.len, textbox.maxLen)]
 
-proc `text=`*(text: var Textbox, newText: seq[Rune]) =
-  text.text = newText[0 ..< min(newText.len, text.maxLen)]
-
-  text.processedText = text.text
-  if text.maxWidth > 0:
-    text.processedText = WordWrapped(
-      text.processedText,
-      text.font[],
-      text.maxWidth / text.shaderdata.data.textbox.data.scale,
+  textbox.processedText = textbox.text
+  if textbox.maxWidth > 0:
+    textbox.processedText = WordWrapped(
+      textbox.processedText,
+      textbox.font[],
+      textbox.maxWidth / textbox.shaderdata.data.textbox.data.scale,
     )
 
-proc `text=`*(text: var Textbox, newText: string) =
-  `text=`(text, newText.toRunes)
+proc `text=`*(textbox: var Textbox, newText: string) =
+  `text=`(textbox, newText.toRunes)
+
+proc Color*(textbox: Textbox): Vec4f =
+  textbox.shaderdata.data.textbox.data.color
 
-proc Color*(text: Textbox): Vec4f =
-  text.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 `Color=`*(text: var Textbox, value: Vec4f) =
-  if text.shaderdata.data.textbox.data.color != value:
-    text.dirtyShaderdata = true
-    text.shaderdata.data.textbox.data.color = value
+proc Scale*(textbox: Textbox): float32 =
+  textbox.shaderdata.data.textbox.data.scale
 
-proc Scale*(text: Textbox): float32 =
-  text.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 AspectRatio*(textbox: Textbox): float32 =
+  textbox.shaderdata.data.textbox.data.aspectratio
 
-proc `Scale=`*(text: var Textbox, value: float32) =
-  if text.shaderdata.data.textbox.data.scale != value:
-    text.dirtyShaderdata = true
-    text.shaderdata.data.textbox.data.scale = value
+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*(text: Textbox): Vec3f =
-  text.shaderdata.data.textbox.data.position
+proc Position*(textbox: Textbox): Vec3f =
+  textbox.shaderdata.data.textbox.data.position
 
-proc `Position=`*(text: var Textbox, value: Vec3f) =
-  if text.shaderdata.data.textbox.data.position != value:
-    text.dirtyShaderdata = true
-    text.shaderdata.data.textbox.data.position = value
+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*(text: Textbox): HorizontalAlignment =
-  text.horizontalAlignment
-proc `horizontalAlignment=`*(text: var Textbox, value: HorizontalAlignment) =
-  if value != text.horizontalAlignment:
-    text.horizontalAlignment = value
-    text.dirtyGeometry = true
+proc horizontalAlignment*(textbox: Textbox): HorizontalAlignment =
+  textbox.horizontalAlignment
+proc `horizontalAlignment=`*(textbox: var Textbox, value: HorizontalAlignment) =
+  if value != textbox.horizontalAlignment:
+    textbox.horizontalAlignment = value
+    textbox.dirtyGeometry = true
 
-proc verticalAlignment*(text: Textbox): VerticalAlignment =
-  text.verticalAlignment
-proc `verticalAlignment=`*(text: var Textbox, value: VerticalAlignment) =
-  if value != text.verticalAlignment:
-    text.verticalAlignment = value
-    text.dirtyGeometry = true
+proc verticalAlignment*(textbox: Textbox): VerticalAlignment =
+  textbox.verticalAlignment
+proc `verticalAlignment=`*(textbox: var Textbox, value: VerticalAlignment) =
+  if value != textbox.verticalAlignment:
+    textbox.verticalAlignment = value
+    textbox.dirtyGeometry = true
+
+proc Refresh*(textbox: var Textbox, aspectratio: float32) =
+  `AspectRatio=`(textbox, aspectratio)
 
-proc Draw(text: Textbox, commandbuffer: VkCommandBuffer, pipeline: Pipeline, currentFiF: int) =
+  if textbox.dirtyShaderdata:
+    textbox.RefreshShaderdata()
+    textbox.dirtyShaderdata = false
+
+  if textbox.dirtyGeometry or textbox.processedText != textbox.lastRenderedText:
+    textbox.RefreshGeometry()
+    textbox.dirtyGeometry = false
+
+proc Render*(textbox: Textbox, commandbuffer: VkCommandBuffer, pipeline: Pipeline, currentFiF: int) =
   WithBind(commandbuffer, (textbox.shaderdata, ), pipeline, currentFiF):
-    Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = text)
+    Render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = textbox)
 
-proc InitTextbox*(
+proc InitTextbox*[T: string | seq[Rune]](
   renderdata: var RenderData,
   descriptorSetLayout: VkDescriptorSetLayout,
   font: Font,
-  text = "".toRunes,
+  text: T = default(T),
   scale: float32 = 1,
   position: Vec3f = NewVec3f(),
   color: Vec4f = NewVec4f(0, 0, 0, 1),
@@ -186,6 +198,7 @@
     maxLen: maxLen,
     font: font,
     dirtyGeometry: true,
+    dirtyShaderdata: true,
     horizontalAlignment: horizontalAlignment,
     verticalAlignment: verticalAlignment,
     maxWidth: maxWidth,
@@ -198,6 +211,7 @@
           scale: scale,
           position: position,
           color: color,
+          aspectratio: 1,
     ), UniformBufferMapped),
     fontAtlas: font.fontAtlas
   )
@@ -213,10 +227,15 @@
     result.indices.data[i * 6 + 4] = vertexIndex + 3
     result.indices.data[i * 6 + 5] = vertexIndex + 0
 
-  `text=`(result, text)
+  when T is string:
+    `text=`(result, text.toRunes())
+  else:
+    `text=`(result, text)
 
-  AssignBuffers(renderdata, result)
+  AssignBuffers(renderdata, result, uploadData = false)
   UploadImages(renderdata, result.shaderdata)
   InitDescriptorSet(renderdata, descriptorSetLayout, result.shaderdata)
 
-  result.Refresh()
+  result.Refresh(1)
+  UpdateAllGPUBuffers(result, flush = true)
+  UpdateAllGPUBuffers(result.shaderdata.data, flush = true)
Binary file tests/resources/default/Overhaul.ttf has changed
Binary file tests/resources/default/donut.glb has changed
Binary file tests/resources/default/key.ogg has changed
Binary file tests/resources/default/test1.glb has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_text.nim	Sat Jul 20 17:45:44 2024 +0700
@@ -0,0 +1,56 @@
+import std/os
+import std/sequtils
+import std/monotimes
+import std/times
+import std/options
+import std/random
+
+import ../semiconginev2
+
+proc test_01_static_label(time: float32, swapchain: var Swapchain) =
+  var renderdata = InitRenderData()
+
+  # scale: float32 = 1,
+  # position: Vec3f = NewVec3f(),
+  # color: Vec4f = NewVec4f(0, 0, 0, 1),
+
+  var pipeline = CreatePipeline[DefaultFontShader](renderPass = swapchain.renderPass)
+
+  var font = LoadFont("Overhaul.ttf", lineHeightPixels = 160)
+  var label1 = InitTextbox(
+    renderdata,
+    pipeline.descriptorSetLayouts[0],
+    font,
+    "Hello semicongine!",
+    color = NewVec4f(1, 1, 1, 1),
+    scale = 0.0005,
+  )
+
+  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)):
+        WithPipeline(commandbuffer, pipeline):
+          Render(label1, commandbuffer, pipeline, swapchain.currentFiF)
+
+        # cleanup
+  checkVkResult vkDeviceWaitIdle(vulkan.device)
+  DestroyPipeline(pipeline)
+  DestroyRenderData(renderdata)
+
+when isMainModule:
+  var time = 10'f32
+  InitVulkan()
+
+  var renderpass = CreateDirectPresentationRenderPass(depthBuffer = true)
+  var swapchain = InitSwapchain(renderpass = renderpass).get()
+
+  # tests a simple triangle with minimalistic shader and vertex format
+  test_01_static_label(time, swapchain)
+
+  checkVkResult vkDeviceWaitIdle(vulkan.device)
+  vkDestroyRenderPass(vulkan.device, renderpass.vk, nil)
+  DestroySwapchain(swapchain)
+
+  DestroyVulkan()