# HG changeset patch # User sam # Date 1723877662 -25200 # Node ID 373a4888f6acd7fe645c94fb772cd511a1732f90 # Parent 41f3612ef38d89f86caee1814289fdde6cfaabc6 did: rework font-rendering diff -r 41f3612ef38d -r 373a4888f6ac semicongine/rendering.nim --- a/semicongine/rendering.nim Sat Aug 17 11:34:15 2024 +0700 +++ b/semicongine/rendering.nim Sat Aug 17 13:54:22 2024 +0700 @@ -159,6 +159,11 @@ proc `=copy`[T](dest: var Pipeline[T]; source: Pipeline[T]) {.error.} proc `=copy`[T](dest: var DescriptorSetData[T]; source: DescriptorSetData[T]) {.error.} +proc `[]`*[T, S](a: GPUArray[T, S], i: int): T = + a.data[i] +proc `[]=`*[T, S](a: var GPUArray[T, S], i: int, value: T) = + a.data[i] = value + template forDescriptorFields(shader: typed, valuename, typename, countname, bindingNumber, body: untyped): untyped = var `bindingNumber` {.inject.} = 0'u32 for theFieldname, `valuename` in fieldPairs(shader): diff -r 41f3612ef38d -r 373a4888f6ac semicongine/rendering/renderer.nim --- a/semicongine/rendering/renderer.nim Sat Aug 17 11:34:15 2024 +0700 +++ b/semicongine/rendering/renderer.nim Sat Aug 17 13:54:22 2024 +0700 @@ -52,8 +52,8 @@ template needsMapping(gpuData: GPUData): untyped = gpuData.bufferType.needsMapping -template size(gpuArray: GPUArray): uint64 = - (gpuArray.data.len * sizeof(elementType(gpuArray.data))).uint64 +template size(gpuArray: GPUArray, count=0'u64): uint64 = + (if count == 0: gpuArray.data.len.uint64 else: count).uint64 * sizeof(elementType(gpuArray.data)).uint64 template size(gpuValue: GPUValue): uint64 = sizeof(gpuValue.data).uint64 func size(image: Image): uint64 = @@ -276,17 +276,23 @@ result.rawPointer = selectedBlock.rawPointer.pointerAddOffset(selectedBlock.offsetNextFree) renderData.memory[memoryType][selectedBlockI].offsetNextFree += memoryRequirements.size -proc updateGPUBuffer*(gpuData: GPUData, flush = false) = - if gpuData.size == 0: +proc updateGPUBuffer*(gpuData: GPUData, count=0'u64, flush = false) = + if gpuData.size() == 0: return when needsMapping(gpuData): - copyMem(pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset), gpuData.rawPointer, gpuData.size) + when gpuData is GPUArray: + copyMem(pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset), gpuData.rawPointer, gpuData.size(count)) + else: + copyMem(pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset), gpuData.rawPointer, gpuData.size()) if flush: flushBuffer(gpuData.buffer) else: withStagingBuffer((gpuData.buffer.vk, gpuData.offset), gpuData.size, stagingPtr): - copyMem(stagingPtr, gpuData.rawPointer, gpuData.size) + when gpuData is GPUArray: + copyMem(stagingPtr, gpuData.rawPointer, gpuData.size(count)) + else: + copyMem(stagingPtr, gpuData.rawPointer, gpuData.size()) proc updateAllGPUBuffers*[T](value: T, flush = false) = for name, fieldvalue in value.fieldPairs(): diff -r 41f3612ef38d -r 373a4888f6ac semicongine/text.nim --- a/semicongine/text.nim Sat Aug 17 11:34:15 2024 +0700 +++ b/semicongine/text.nim Sat Aug 17 13:54:22 2024 +0700 @@ -43,19 +43,16 @@ color: Vec4f position: Vec3f scale: float32 - aspectratio: float32 - TextboxDescriptorSet = object - textbox: GPUValue[TextboxData, UniformBufferMapped] - fontAtlas: Image[Gray] - DefaultFontShader* = object + DefaultFontShader*[T] = object position {.VertexAttribute.}: Vec3f uv {.VertexAttribute.}: Vec2f # TODO: maybe we can keep the uvs in a uniform buffer and just pass an index fragmentUv {.Pass.}: Vec2f color {.ShaderOutput.}: Vec4f - descriptorSets {.DescriptorSet: 0.}: TextboxDescriptorSet + textbox {.PushConstant.}: TextboxData + descriptorSets {.DescriptorSet: 0.}: T vertexCode* = """void main() { - gl_Position = vec4(position * vec3(1 / textbox.aspectratio, 1, 1) * textbox.scale + textbox.position, 1.0); + gl_Position = vec4(position * textbox.scale + textbox.position, 1.0); fragmentUv = uv; } """ fragmentCode* = """void main() { @@ -66,6 +63,7 @@ color = vec4(textbox.color.rgb, textbox.color.a * v); }""" +proc `=copy`(dest: var FontObj; source: FontObj) {.error.} include ./text/font include ./text/textbox diff -r 41f3612ef38d -r 373a4888f6ac semicongine/text/textbox.nim --- a/semicongine/text/textbox.nim Sat Aug 17 11:34:15 2024 +0700 +++ b/semicongine/text/textbox.nim Sat Aug 17 13:54:22 2024 +0700 @@ -3,43 +3,39 @@ font*: Font maxLen*: int # maximum amount of characters that will be rendered maxWidth: float32 = 0 # if set, will cause automatic word breaks at maxWidth - # properties: + baseScale: float32 text: seq[Rune] horizontalAlignment: HorizontalAlignment = Center verticalAlignment: VerticalAlignment = Center # management/internal: dirtyGeometry: bool # is true if any of the attributes changed dirtyShaderdata: bool # is true if any of the attributes changed - processedText: seq[Rune] # used to store processed (word-wrapper) text to preserve original + visibleText: seq[Rune] # used to store processed (word-wrapper) text to preserve original 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: DescriptorSetData[TextboxDescriptorSet] proc `=copy`(dest: var Textbox; source: Textbox) {.error.} func `$`*(textbox: Textbox): string = "\"" & $textbox.text[0 ..< min(textbox.text.len, 16)] & "\"" -proc refreshShaderdata(textbox: Textbox) = - textbox.shaderdata.data.textbox.updateGPUBuffer(flush = true) - proc refreshGeometry(textbox: var Textbox) = # pre-calculate text-width var width = 0'f32 var lineWidths: seq[float32] - for i in 0 ..< textbox.processedText.len: - if textbox.processedText[i] == NEWLINE: + for i in 0 ..< textbox.visibleText.len: + if textbox.visibleText[i] == NEWLINE: lineWidths.add width width = 0'f32 else: - 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])] + if not (i == textbox.visibleText.len - 1 and textbox.visibleText[i].isWhiteSpace): + width += textbox.font.glyphs[textbox.visibleText[i]].advance + if i < textbox.visibleText.len - 1: + width += textbox.font.kerning[(textbox.visibleText[i], textbox.visibleText[i + 1])] lineWidths.add width var height = float32(lineWidths.len - 1) * textbox.font.lineAdvance + textbox.font.capHeight if lineWidths[^1] == 0 and lineWidths.len > 1: @@ -61,8 +57,8 @@ of Right: lineWidths[lineIndex] for i in 0 ..< textbox.maxLen: let vertexOffset = i * 4 - if i < textbox.processedText.len: - if textbox.processedText[i] == Rune('\n'): + if i < textbox.visibleText.len: + if textbox.visibleText[i] == Rune('\n'): offsetX = 0 offsetY -= textbox.font.lineAdvance textbox.position.data[vertexOffset + 0] = vec3(0, 0, 0) @@ -76,7 +72,7 @@ of Right: lineWidths[lineIndex] else: let - glyph = textbox.font.glyphs[textbox.processedText[i]] + glyph = textbox.font.glyphs[textbox.visibleText[i]] left = offsetX + glyph.leftOffset right = offsetX + glyph.leftOffset + glyph.dimension.x top = offsetY - glyph.topOffset @@ -93,16 +89,11 @@ textbox.uv.data[vertexOffset + 3] = glyph.uvs[3] offsetX += glyph.advance - if i < textbox.processedText.len - 1: - offsetX += textbox.font.kerning[(textbox.processedText[i], textbox.processedText[i + 1])] - else: - textbox.position.data[vertexOffset + 0] = vec3(0, 0, 0) - textbox.position.data[vertexOffset + 1] = vec3(0, 0, 0) - textbox.position.data[vertexOffset + 2] = vec3(0, 0, 0) - textbox.position.data[vertexOffset + 3] = vec3(0, 0, 0) - updateGPUBuffer(textbox.position) - updateGPUBuffer(textbox.uv) - textbox.lastRenderedText = textbox.processedText + if i < textbox.visibleText.len - 1: + offsetX += textbox.font.kerning[(textbox.visibleText[i], textbox.visibleText[i + 1])] + updateGPUBuffer(textbox.position, count=textbox.visibleText.len.uint64 * 4) + updateGPUBuffer(textbox.uv, count=textbox.visibleText.len.uint64 * 4) + textbox.lastRenderedText = textbox.visibleText func text*(textbox: Textbox): seq[Rune] = textbox.text @@ -113,41 +104,17 @@ textbox.text = newText[0 ..< min(newText.len, textbox.maxLen)] - textbox.processedText = textbox.text + textbox.visibleText = textbox.text if textbox.maxWidth > 0: - textbox.processedText = WordWrapped( - textbox.processedText, + textbox.visibleText = WordWrapped( + textbox.visibleText, textbox.font[], - textbox.maxWidth / textbox.shaderdata.data.textbox.data.scale, + textbox.maxWidth / textbox.baseScale, ) proc `text=`*(textbox: var Textbox, newText: string) = `text=`(textbox, newText.toRunes) -proc color*(textbox: Textbox): Vec4f = - textbox.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 scale*(textbox: Textbox): float32 = - textbox.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 position*(textbox: Textbox): Vec3f = - textbox.shaderdata.data.textbox.data.position - -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*(textbox: Textbox): HorizontalAlignment = textbox.horizontalAlignment proc `horizontalAlignment=`*(textbox: var Textbox, value: HorizontalAlignment) = @@ -163,34 +130,36 @@ textbox.dirtyGeometry = true proc refresh*(textbox: var Textbox) = - if textbox.shaderdata.data.textbox.data.aspectratio != getAspectRatio(): - textbox.dirtyShaderdata = true - textbox.shaderdata.data.textbox.data.aspectratio = getAspectRatio() - - if textbox.dirtyShaderdata: - textbox.refreshShaderdata() - textbox.dirtyShaderdata = false - - if textbox.dirtyGeometry or textbox.processedText != textbox.lastRenderedText: + if textbox.dirtyGeometry or textbox.visibleText != textbox.lastRenderedText: textbox.refreshGeometry() textbox.dirtyGeometry = false -proc render*(commandbuffer: VkCommandBuffer, pipeline: Pipeline, textbox: Textbox) = - bindDescriptorSet(commandbuffer, textbox.shaderdata, 0, pipeline) - render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = textbox) +proc render*( + commandbuffer: VkCommandBuffer, + pipeline: Pipeline, + textbox: Textbox, + position: Vec3f, + color: Vec4f, + scale: float32 = 1, +) = + renderWithPushConstant( + commandbuffer = commandbuffer, + pipeline = pipeline, + mesh = textbox, + pushConstant = TextboxData(position: position, scale: textbox.baseScale * scale, color: color), + fixedVertexCount=textbox.visibleText.len * 6 + ) proc initTextbox*[T: string | seq[Rune]]( renderdata: var RenderData, descriptorSetLayout: VkDescriptorSetLayout, font: Font, + baseScale: float32, text: T = default(T), - scale: float32 = 1, - position: Vec3f = vec3(0, 0, 0), - color: Vec4f = vec4(0, 0, 0, 1), maxLen: int = text.len, verticalAlignment: VerticalAlignment = Center, horizontalAlignment: HorizontalAlignment = Center, - maxWidth = 0'f32 + maxWidth = 0'f32, ): Textbox = result = Textbox( @@ -201,20 +170,10 @@ horizontalAlignment: horizontalAlignment, verticalAlignment: verticalAlignment, maxWidth: maxWidth, + baseScale: baseScale, position: asGPUArray(newSeq[Vec3f](int(maxLen * 4)), VertexBuffer), uv: asGPUArray(newSeq[Vec2f](int(maxLen * 4)), VertexBuffer), indices: asGPUArray(newSeq[uint16](int(maxLen * 6)), IndexBuffer), - shaderdata: asDescriptorSetData( - TextboxDescriptorSet( - textbox: asGPUValue(TextboxData( - scale: scale, - position: position, - color: color, - aspectratio: 1, - ), UniformBufferMapped), - fontAtlas: font.fontAtlas - ) - ) ) for i in 0 ..< maxLen: @@ -232,9 +191,6 @@ `text=`(result, text) assignBuffers(renderdata, result, uploadData = false) - uploadImages(renderdata, result.shaderdata) - initDescriptorSet(renderdata, descriptorSetLayout, result.shaderdata) result.refresh() updateAllGPUBuffers(result, flush = true) - updateAllGPUBuffers(result.shaderdata.data, flush = true) diff -r 41f3612ef38d -r 373a4888f6ac tests/test_text.nim --- a/tests/test_text.nim Sat Aug 17 11:34:15 2024 +0700 +++ b/tests/test_text.nim Sat Aug 17 13:54:22 2024 +0700 @@ -9,28 +9,39 @@ import ../semicongine +type + FontDS = object + fontAtlas: Image[Gray] + proc test_01_static_label(time: float32) = + var font = loadFont("Overhaul.ttf", lineHeightPixels = 160) var renderdata = initRenderData() + var pipeline = createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass) - var pipeline = createPipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass) + var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas)) + uploadImages(renderdata, ds) + initDescriptorSet( + renderdata, + pipeline.layout(0), + ds, + ) - var font = loadFont("Overhaul.ttf", lineHeightPixels = 160) var label1 = initTextbox( renderdata, pipeline.layout(0), font, + 0.0005, "Hello semicongine!", - color = vec4(1, 1, 1, 1), - scale = 0.0005, ) var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: label1.refresh() withNextFrame(framebuffer, commandbuffer): + bindDescriptorSet(commandbuffer, ds, 0, pipeline) withRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, vec4(0, 0, 0, 0)): withPipeline(commandbuffer, pipeline): - render(commandbuffer, pipeline, label1) + render(commandbuffer, pipeline, label1, vec3(), vec4(1, 1, 1, 1)) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device) @@ -38,40 +49,46 @@ destroyRenderData(renderdata) proc test_02_multiple_animated(time: float32) = - var renderdata = initRenderData() - - var pipeline = createPipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass) - var font1 = loadFont("Overhaul.ttf", lineHeightPixels = 40) var font2 = loadFont("Overhaul.ttf", lineHeightPixels = 160) var font3 = loadFont("DejaVuSans.ttf", lineHeightPixels = 160) + var renderdata = initRenderData() + + var pipeline = createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass) + + var ds1 = asDescriptorSetData(FontDS(fontAtlas: font1.fontAtlas)) + uploadImages(renderdata, ds1) + initDescriptorSet(renderdata, pipeline.layout(0), ds1) + + var ds2 = asDescriptorSetData(FontDS(fontAtlas: font2.fontAtlas)) + uploadImages(renderdata, ds2) + initDescriptorSet(renderdata, pipeline.layout(0), ds2) + + var ds3 = asDescriptorSetData(FontDS(fontAtlas: font3.fontAtlas)) + uploadImages(renderdata, ds3) + initDescriptorSet(renderdata, pipeline.layout(0), ds3) + var labels = [ initTextbox( renderdata, pipeline.layout(0), font1, + 0.004, " 0", - color = vec4(0, 1, 1, 1), - scale = 0.004, - position = vec3(-0.3, 0.5) ), initTextbox( renderdata, pipeline.layout(0), font2, + 0.001, " 1", - color = vec4(1, 0, 1, 1), - scale = 0.001, - position = vec3(0, 0) ), initTextbox( renderdata, pipeline.layout(0), font3, + 0.001, " 2", - color = vec4(1, 1, 0, 1), - scale = 0.001, - position = vec3(0.3, -0.5) ) ] @@ -80,19 +97,36 @@ while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time for i in 0 ..< labels.len: - var c = labels[i].color - c[i] = progress - labels[i].color = c - labels[i].scale = labels[i].scale * (1.0 + (i + 1).float * 0.001) - labels[i].position = labels[i].position + vec3(0.001 * (i.float - 1'f)) labels[i].text = $(p + i) labels[i].refresh() inc p withNextFrame(framebuffer, commandbuffer): withRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, vec4(0, 0, 0, 0)): withPipeline(commandbuffer, pipeline): - for label in labels.litems: - render(commandbuffer, pipeline, label) + bindDescriptorSet(commandbuffer, ds1, 0, pipeline) + render( + commandbuffer, + pipeline, + labels[0], + position=vec3(0 / labels.len, 0.1 + progress * 0.5), + color=vec4(1, 1, 1, 1), + ) + bindDescriptorSet(commandbuffer, ds2, 0, pipeline) + render( + commandbuffer, + pipeline, + labels[1], + position=vec3(1 / labels.len, 0.1 + progress * 0.5), + color=vec4(1, 1, 1, 1), + ) + bindDescriptorSet(commandbuffer, ds3, 0, pipeline) + render( + commandbuffer, + pipeline, + labels[2], + position=vec3(2 / labels.len, 0.1 + progress * 0.5), + color=vec4(1, 1, 1, 1), + ) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device) @@ -100,11 +134,19 @@ destroyRenderData(renderdata) proc test_03_layouting(time: float32) = + var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 40) var renderdata = initRenderData() - var pipeline = createPipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass) + var pipeline = createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass) - var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 40) + var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas)) + uploadImages(renderdata, ds) + initDescriptorSet( + renderdata, + pipeline.layout(0), + ds, + ) + var labels: seq[Textbox] for horizontal in HorizontalAlignment: @@ -112,10 +154,8 @@ renderdata, pipeline.layout(0), font, + 0.001, $horizontal & " aligned", - color = vec4(1, 1, 1, 1), - scale = 0.001, - position = vec3(0, 0.9 - (horizontal.float * 0.15)), horizontalAlignment = horizontal, ) for vertical in VerticalAlignment: @@ -123,37 +163,33 @@ renderdata, pipeline.layout(0), font, + 0.001, $vertical & " aligned", - color = vec4(1, 1, 1, 1), - scale = 0.001, - position = vec3(-0.35 + (vertical.float * 0.35), 0.3), verticalAlignment = vertical, ) labels.add initTextbox( renderdata, pipeline.layout(0), font, + 0.001, """Paragraph This is a somewhat longer paragraph with a few newlines and a maximum width of 0.2. It should display with some space above and have a pleasing appearance overall! :)""", maxWidth = 0.6, - color = vec4(1, 1, 1, 1), - scale = 0.001, - position = vec3(-0.9, 0.1), verticalAlignment = Top, horizontalAlignment = Left, ) - var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: let progress = ((getMonoTime() - start).inMilliseconds().int / 1000) / time withNextFrame(framebuffer, commandbuffer): + bindDescriptorSet(commandbuffer, ds, 0, pipeline) withRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, vec4(0, 0, 0, 0)): withPipeline(commandbuffer, pipeline): - for label in labels: - render(commandbuffer, pipeline, label) + for i in 0 ..< labels.len: + render(commandbuffer, pipeline, labels[i], vec3(0.5 - i.float32 * 0.1, 0.5 - i.float32 * 0.1), vec4(1, 1, 1, 1)) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device) @@ -161,33 +197,45 @@ destroyRenderData(renderdata) proc test_04_lots_of_texts(time: float32) = + var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 160) var renderdata = initRenderData() - var pipeline = createPipeline[DefaultFontShader](renderPass = vulkan.swapchain.renderPass) + var pipeline = createPipeline[DefaultFontShader[FontDS]](renderPass = vulkan.swapchain.renderPass) - var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 160) + var ds = asDescriptorSetData(FontDS(fontAtlas: font.fontAtlas)) + uploadImages(renderdata, ds) + initDescriptorSet( + renderdata, + pipeline.layout(0), + ds, + ) + var labels: seq[Textbox] + var positions = newSeq[Vec3f](100) + var colors = newSeq[Vec4f](100) + var scales = newSeq[float32](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] = rand(0.5'f32 .. 1.5'f32) labels.add initTextbox( renderdata, pipeline.layout(0), font, + 0.001, $i, - 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.0002 .. 0.002), - position = vec3(rand(-0.5 .. 0.5), rand(-0.5 .. 0.5), rand(-0.1 .. 0.1)) ) - labels.sort(proc(x, y: Textbox): int = cmp(x.position.z, y.position.z), Ascending) var start = getMonoTime() while ((getMonoTime() - start).inMilliseconds().int / 1000) < time: for l in labels.mitems: l.refresh() withNextFrame(framebuffer, commandbuffer): + bindDescriptorSet(commandbuffer, ds, 0, pipeline) withRenderPass(vulkan.swapchain.renderPass, framebuffer, commandbuffer, vulkan.swapchain.width, vulkan.swapchain.height, vec4(0, 0, 0, 0)): withPipeline(commandbuffer, pipeline): - for l in labels: - render(commandbuffer, pipeline, l) + for i in 0 ..< labels.len: + render(commandbuffer, pipeline, labels[i], positions[i], colors[i], scales[i]) # cleanup checkVkResult vkDeviceWaitIdle(vulkan.device)