# HG changeset patch # User sam # Date 1721472344 -25200 # Node ID 17638322012384742eb3e43695acad5d26f33b50 # Parent c70fee6568f61a1e12b42e9e398d4bf0f7d3d1e1 add: first font-rendering test diff -r c70fee6568f6 -r 176383220123 semiconginev2/rendering/renderer.nim --- 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 diff -r c70fee6568f6 -r 176383220123 semiconginev2/resources.nim --- 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) diff -r c70fee6568f6 -r 176383220123 semiconginev2/text.nim --- 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 diff -r c70fee6568f6 -r 176383220123 semiconginev2/text/font.nim --- 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] diff -r c70fee6568f6 -r 176383220123 semiconginev2/text/textbox.nim --- 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) diff -r c70fee6568f6 -r 176383220123 tests/resources/default/Overhaul.ttf Binary file tests/resources/default/Overhaul.ttf has changed diff -r c70fee6568f6 -r 176383220123 tests/resources/default/donut.glb Binary file tests/resources/default/donut.glb has changed diff -r c70fee6568f6 -r 176383220123 tests/resources/default/key.ogg Binary file tests/resources/default/key.ogg has changed diff -r c70fee6568f6 -r 176383220123 tests/resources/default/test1.glb Binary file tests/resources/default/test1.glb has changed diff -r c70fee6568f6 -r 176383220123 tests/test_text.nim --- /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()