formatting, cleanup, more progress on docs

This commit is contained in:
2025-01-11 17:31:32 -05:00
parent e8a7b21eba
commit f7e4278300
9 changed files with 186 additions and 93 deletions

View File

@@ -1,28 +0,0 @@
# Architecture
The purpose of this library to really allieviate four issues with one encapsulation:
* font parsing
* text codepoint shaping
* glyph shape triangulation
* glyph draw-list generation
Shaping text, getting metrics for the glyphs, triangulating glyphs, and anti-aliasing their render are expensive todo per frame. So anything related to that compute that may be cached, will be.
There are two cache types used:
* shape cache (shape_cache.state)
* atlas region cache (Atlas_Region.state)
The shape cache stores all data for a piece of text that will be utilized in a draw call that is not dependent on a specific position & scale (and is faster to lookup vs compute per draw call). So far these are the text shaping itself, and per-glyph infos: atlas_lru_code (atlas key), atlas region resolution, & glyph bounds.
The atlas region cache tracks what slots have glyphs rendered to the texture atlas. This essentially is caching of triangulation and super-sampling compute.
All caching uses the [LRU.odin](../vefontcache/LRU.odin)
## Codepaths
## Library Lifetime
## Draw List Generation
The base draw list generation pipepline provided by the library allows the user to batch whatever the want into a single "layer". However

128
docs/guide_architecture.md Normal file
View File

@@ -0,0 +1,128 @@
# Guide: Architecture
Overview on the state of package design and codepath layout.
---
The purpose of this library to really allieviate four issues with one encapsulation:
* font parsing
* text codepoint shaping
* glyph shape triangulation
* glyph draw-list generation
Shaping text, getting metrics for the glyphs, triangulating glyphs, and anti-aliasing their render are expensive todo per frame. So anything related to that compute that may be cached, will be.
There are two cache types used:
* shape cache (`Shaped_Text_Cache.state`)
* atlas region cache (`Atlas_Region.state`)
The shape cache stores all data for a piece of text that will be utilized in a draw call that is not dependent on a specific position & scale (and is faster to lookup vs compute per draw call). So far these are the text shaping itself, and per-glyph infos: atlas_lru_code (atlas key), atlas region resolution, & glyph bounds.
The atlas region cache tracks what slots have glyphs rendered to the texture atlas. This essentially is caching of triangulation and super-sampling compute.
All caching uses the [LRU.odin](../vefontcache/LRU.odin)
## Codepaths
### Lifetime
The library lifetime is pretty straightfoward, you have a startup to do that should just be called sometime in your usual app start.s. From there you may either choose to manually shut it down or let the OS clean it up.
If hot-reload is desired, you just need to call hot_reload with the context's backing allocator to refresh the procedure references. After the dll has been reloaded those should be the only aspects that have been scrambled.
Usually when hot-reloading the library for tuning or major changes, you'd also want to clear the caches. So just call the `clear_atlas_region_caches` & `clear_shape_cache` right after.
Any scratch memory used for draw list generation is kept persistently in the library's `Context`. I wanted to avoid any dynamic allocation slowness as its an extremely hot path.
Ideally there should be zero dynamic allocation on a per-frame basis so long as the reserves for the dynamic containers are never exceeded. Its alright if they do as their memory locality is so large their distance in the pages to load into cpu cache won't matter, just needs to be a low incidence.
### Shaping pass
If the user is using the library's cache, then at some point `shaper_shape_text_cached` which handles the hasing and lookup. So long as a shape is found it will not enter uncached codepath. By default this library uses `shaper_shape_harfbuzz` as the `shape_text_uncached` procedure.
Shapes are cached using the following parameters to hash a key:
* font: Font_ID
* font_size: f32
* the text itself: string
All shapers fullfill the following interface:
```odin
Shaper_Shape_Text_Uncached_Proc :: #type proc( ctx : ^Shaper_Context,
atlas : Atlas,
glyph_buffer_size : Vec2,
font : Font_ID,
entry : Entry,
font_px_Size : f32,
font_scale : f32,
text_utf8 : string,
output : ^Shaped_Text
)
```
Which will resolve the output `Shaped_Text`. Which has the following structure:
```odin
Shaped_Text :: struct #packed {
glyph : [dynamic]Glyph,
position : [dynamic]Vec2,
atlas_lru_code : [dynamic]Atlas_Key,
region_kind : [dynamic]Atlas_Region_Kind,
bounds : [dynamic]Range2,
end_cursor_pos : Vec2,
size : Vec2,
font : Font_ID,
px_size : f32,
}
```
What is actually the result of the shaping process is the arrays of glyphs and their positions for the the shape or most historically known as: *Slug*, of prepared text for printing. The end position of where the user's "cursor" would be is also recorded which provided the end position of the shape. The size of the shape is also resolved here, which if using px_scalar must be downscaled. `measure_shape_size` does the downscaling for the user.
The font and px_size is tracked here as well so they user does not need to provide it to the library's interface and related.
As stated under the main heading of this guide, the the following are within shaped text so
that they may be resolved outside of the draw list generation (see: `generate_shape_draw_list`)
* atlas_lru_code
* region_kind
* bounds
### Draw List Generation
### On Layering
The base draw list generation pippline provided by the library allows the user to batch whatever the want into a single "layer".
However, the user most likely would want take into consideration: font instances, font size, colors; these are things that may benefit from having shared locality during a layer batch. Overlaping text benefits from the user to handle the ordering via layers.
Layers (so far) are just a set of offssets tracked by the library's `Context.draw_layer` struct. When `flush_draw_list_layer` is called, the offsets are set to the current leng of the draw list. This allows the rendering backend to retrieve the latest set of vertices, indices, and calls to render on a per-layer basis with: `get_draw_list_layer`.
Importantly, this leads to the following pattern when enuquing a layer to render:
1. Begin render pass
2. For codepath that will deal with text layers
1. Process user-level code-path that calls the draw text interface, populating the draw list layer (usually a for loop)
2. After iteration on the layer is complete render the text layer
1. grab the draw list layer
2. flush the layer so the draw list offsets are reset
3. Repeat until all layers for the codepath are exhausted.
There is consideration to instead explicitly have a draw list with more contextual information of the start and end of each layer. So that batching can be orchestrated in a section of their pipline.
This would involve just tracking *slices* of thier draw-list that represents layers:
```odin
Draw_List_Layer :: struct {
vertices : []Vertex,
indices : []u32,
calls : []Draw_Call,
}
```
Eventually the library may provide this since adding that feature is relatively cheap and and a low line-count addition to the interface.
There should be little to no perfomrance loss from doing so as the iteration size is two large of a surface area to matter (so its just pipeline ergonomics)

View File

@@ -12,8 +12,6 @@ package vefontcache
are marginal changes at best. are marginal changes at best.
*/ */
import "base:runtime"
// 16-bit hashing was attempted, however it seems to get collisions with djb8_hash_16 // 16-bit hashing was attempted, however it seems to get collisions with djb8_hash_16
LRU_Fail_Mask_16 :: 0xFFFF LRU_Fail_Mask_16 :: 0xFFFF

View File

@@ -4,10 +4,7 @@ package vefontcache
Note(Ed): This may be seperated in the future into another file dedending on how much is involved with supportin ear-clipping triangulation. Note(Ed): This may be seperated in the future into another file dedending on how much is involved with supportin ear-clipping triangulation.
*/ */
import "base:runtime" // import "thirdparty:freetype"
import "base:intrinsics"
import "core:slice"
import "thirdparty:freetype"
Glyph_Trianglation_Method :: enum(i32) { Glyph_Trianglation_Method :: enum(i32) {
Ear_Clipping, Ear_Clipping,
@@ -327,7 +324,7 @@ generate_shape_draw_list :: proc( draw_list : ^Draw_List, shape : Shaped_Text,
append_sub_pack :: #force_inline proc ( pack : ^[dynamic]i32, entry : i32 ) append_sub_pack :: #force_inline proc ( pack : ^[dynamic]i32, entry : i32 )
{ {
raw := cast(^runtime.Raw_Dynamic_Array) pack raw := cast(^Raw_Dynamic_Array) pack
raw.len += 1 raw.len += 1
pack[len(pack) - 1] = entry pack[len(pack) - 1] = entry
} }

View File

@@ -5,9 +5,7 @@ package vefontcache
Just a bunch of utilities. Just a bunch of utilities.
*/ */
import "base:runtime"
import "core:simd" import "core:simd"
import "core:math"
import core_log "core:log" import core_log "core:log"
@@ -16,17 +14,17 @@ peek_array :: #force_inline proc "contextless" ( self : [dynamic]$Type ) -> Type
} }
reload_array :: #force_inline proc( self : ^[dynamic]$Type, allocator : Allocator ) { reload_array :: #force_inline proc( self : ^[dynamic]$Type, allocator : Allocator ) {
raw := transmute( ^runtime.Raw_Dynamic_Array) self raw := transmute( ^Raw_Dynamic_Array) self
raw.allocator = allocator raw.allocator = allocator
} }
reload_array_soa :: #force_inline proc( self : ^#soa[dynamic]$Type, allocator : Allocator ) { reload_array_soa :: #force_inline proc( self : ^#soa[dynamic]$Type, allocator : Allocator ) {
raw := runtime.raw_soa_footer(self) raw := raw_soa_footer(self)
raw.allocator = allocator raw.allocator = allocator
} }
reload_map :: #force_inline proc( self : ^map [$KeyType] $EntryType, allocator : Allocator ) { reload_map :: #force_inline proc( self : ^map [$KeyType] $EntryType, allocator : Allocator ) {
raw := transmute( ^runtime.Raw_Map) self raw := transmute( ^Raw_Map) self
raw.allocator = allocator raw.allocator = allocator
} }

View File

@@ -15,11 +15,7 @@ TODO(Ed): Just keep a local version of stb_truetype and modify it to support a s
Already wanted to do so anyway to evaluate the shape generation implementation. Already wanted to do so anyway to evaluate the shape generation implementation.
*/ */
import "base:runtime"
import "core:c" import "core:c"
import "core:math"
import "core:mem"
import "core:slice"
import stbtt "thirdparty:stb/truetype" import stbtt "thirdparty:stb/truetype"
// import freetype "thirdparty:freetype" // import freetype "thirdparty:freetype"
@@ -74,7 +70,7 @@ parser_stbtt_allocator_proc :: proc(
) -> rawptr ) -> rawptr
{ {
allocator := transmute(^Allocator) allocator_data allocator := transmute(^Allocator) allocator_data
result, error := allocator.procedure( allocator.data, cast(mem.Allocator_Mode) type, cast(int) size, cast(int) alignment, old_memory, cast(int) old_size ) result, error := allocator.procedure( allocator.data, cast(Allocator_Mode) type, cast(int) size, cast(int) alignment, old_memory, cast(int) old_size )
assert(error == .None) assert(error == .None)
if type == .Alloc || type == .Resize { if type == .Alloc || type == .Resize {

View File

@@ -3,6 +3,10 @@ package vefontcache
import "base:builtin" import "base:builtin"
resize_soa_non_zero :: non_zero_resize_soa resize_soa_non_zero :: non_zero_resize_soa
import "base:runtime" import "base:runtime"
Raw_Dynamic_Array :: runtime.Raw_Dynamic_Array
Raw_Map :: runtime.Raw_Map
raw_soa_footer :: runtime.raw_soa_footer
nil_allocator :: runtime.nil_allocator
import "core:hash" import "core:hash"
ginger16 :: hash.ginger16 ginger16 :: hash.ginger16
import "core:math" import "core:math"
@@ -32,6 +36,7 @@ import "core:mem"
Allocator :: mem.Allocator Allocator :: mem.Allocator
Allocator_Error :: mem.Allocator_Error Allocator_Error :: mem.Allocator_Error
Allocator_Mode :: mem.Allocator_Mode
Arena :: mem.Arena Arena :: mem.Arena
arena_allocator :: mem.arena_allocator arena_allocator :: mem.arena_allocator

View File

@@ -32,8 +32,8 @@ Shaped_Text :: struct #packed {
bounds : [dynamic]Range2, bounds : [dynamic]Range2,
end_cursor_pos : Vec2, end_cursor_pos : Vec2,
size : Vec2, size : Vec2,
font_id : Font_ID, font : Font_ID,
// TODO(Ed): We need to track the font here for usage in user interface when directly drawing the shape. px_size : f32,
} }
// Ease of use cache, can handle thousands of lookups per frame with ease. // Ease of use cache, can handle thousands of lookups per frame with ease.
@@ -48,6 +48,7 @@ Shaped_Text_Cache :: struct {
Shaper_Shape_Text_Uncached_Proc :: #type proc( ctx : ^Shaper_Context, Shaper_Shape_Text_Uncached_Proc :: #type proc( ctx : ^Shaper_Context,
atlas : Atlas, atlas : Atlas,
glyph_buffer_size : Vec2, glyph_buffer_size : Vec2,
font : Font_ID,
entry : Entry, entry : Entry,
font_px_Size : f32, font_px_Size : f32,
font_scale : f32, font_scale : f32,
@@ -111,6 +112,7 @@ shaper_unload_font :: #force_inline proc( info : ^Shaper_Info )
shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context, shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context,
atlas : Atlas, atlas : Atlas,
glyph_buffer_size : Vec2, glyph_buffer_size : Vec2,
font : Font_ID,
entry : Entry, entry : Entry,
font_px_size : f32, font_px_size : f32,
font_scale : f32, font_scale : f32,
@@ -297,6 +299,9 @@ shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context,
output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled ) output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled )
} }
profile_end() profile_end()
output.font = font
output.px_size = font_px_size
return return
} }
@@ -305,6 +310,7 @@ shaper_shape_harfbuzz :: proc( ctx : ^Shaper_Context,
shaper_shape_text_latin :: proc( ctx : ^Shaper_Context, shaper_shape_text_latin :: proc( ctx : ^Shaper_Context,
atlas : Atlas, atlas : Atlas,
glyph_buffer_size : Vec2, glyph_buffer_size : Vec2,
font : Font_ID,
entry : Entry, entry : Entry,
font_px_size : f32, font_px_size : f32,
font_scale : f32, font_scale : f32,
@@ -388,6 +394,9 @@ shaper_shape_text_latin :: proc( ctx : ^Shaper_Context,
output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled ) output.region_kind[index] = atlas_decide_region( atlas, glyph_buffer_size, bounds_size_scaled )
} }
profile_end() profile_end()
output.font = font
output.px_size = font_px_size
} }
// Shapes are tracked by the library's context using the shape cache // Shapes are tracked by the library's context using the shape cache
@@ -442,7 +451,7 @@ shaper_shape_text_cached :: proc( text_utf8 : string,
} }
storage_entry := & shape_cache.storage[ shape_cache_idx ] storage_entry := & shape_cache.storage[ shape_cache_idx ]
shape_text_uncached( ctx, atlas, glyph_buffer_size, entry, font_px_size, font_scale, text_utf8, storage_entry ) shape_text_uncached( ctx, atlas, glyph_buffer_size, font, entry, font_px_size, font_scale, text_utf8, storage_entry )
shaped_text = storage_entry ^ shaped_text = storage_entry ^
return return

View File

@@ -3,8 +3,6 @@ See: https://github.com/Ed94/VEFontCache-Odin
*/ */
package vefontcache package vefontcache
import "base:runtime"
// See: mappings.odin for profiling hookup // See: mappings.odin for profiling hookup
DISABLE_PROFILING :: true DISABLE_PROFILING :: true
ENABLE_OVERSIZED_GLYPHS :: true ENABLE_OVERSIZED_GLYPHS :: true
@@ -661,6 +659,7 @@ shape_text_uncached :: #force_inline proc( ctx : ^Context, font : Font_ID, px_si
shaper_proc(& ctx.shaper_ctx, shaper_proc(& ctx.shaper_ctx,
ctx.atlas, ctx.atlas,
vec2(ctx.glyph_buffer.size), vec2(ctx.glyph_buffer.size),
font,
entry, entry,
target_px_size, target_px_size,
target_font_scale, target_font_scale,
@@ -674,7 +673,7 @@ shape_text_uncached :: #force_inline proc( ctx : ^Context, font : Font_ID, px_si
//#region("draw_list generation") //#region("draw_list generation")
/* The most fundamental interface-level draw shape procedure. /* The most basic interface-level draw shape procedure.
Context's stack is not used. Only modifications for alpha sharpen and px_scalar are applied. Context's stack is not used. Only modifications for alpha sharpen and px_scalar are applied.
view, position, and scale are expected to be in unsigned normalized space: view, position, and scale are expected to be in unsigned normalized space:
@@ -698,29 +697,20 @@ shape_text_uncached :: #force_inline proc( ctx : ^Context, font : Font_ID, px_si
<-> scale : Scale the glyph beyond its default scaling from its px_size. <-> scale : Scale the glyph beyond its default scaling from its px_size.
*/ */
@(optimization_mode="favor_size") @(optimization_mode="favor_size")
draw_text_shape_normalized_space :: #force_inline proc( ctx : ^Context, draw_text_shape_normalized_space :: #force_inline proc( ctx : ^Context, colour : RGBAN, position : Vec2, scale : Vec2, shape : Shaped_Text )
font : Font_ID,
px_size : f32,
colour : RGBAN,
position : Vec2,
scale : Vec2,
shape : Shaped_Text
)
{ {
profile(#procedure) profile(#procedure)
assert( ctx != nil ) assert( ctx != nil )
// TODO(Ed): This should be taken from the shape instead (you cannot use a different font with a shape)
assert( font >= 0 && int(font) < len(ctx.entries) )
entry := ctx.entries[ font ] entry := ctx.entries[ shape.font ]
should_alpha_sharpen := cast(f32) cast(i32) (colour.a >= 1.0) should_alpha_sharpen := cast(f32) cast(i32) (colour.a >= 1.0)
adjusted_colour := colour adjusted_colour := colour
adjusted_colour.a += ctx.alpha_sharpen * should_alpha_sharpen adjusted_colour.a += ctx.alpha_sharpen * should_alpha_sharpen
target_px_size := px_size * ctx.px_scalar target_px_size := shape.px_size
target_scale := scale * (1 / ctx.px_scalar) target_scale := scale * (1 / ctx.px_scalar)
target_font_scale := parser_scale( entry.parser_info, target_px_size ) target_font_scale := parser_scale( entry.parser_info, shape.px_size )
ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer,
ctx.px_scalar, ctx.px_scalar,
@@ -733,7 +723,7 @@ draw_text_shape_normalized_space :: #force_inline proc( ctx : ^Context,
) )
} }
/* Non-scoping context. The most fundamental interface-level draw shape procedure (everything else is quality of life warppers). /* Non-scoping context. The most basic interface-level draw shape procedure (everything else is quality of life warppers).
Context's stack is not used. Only modifications for alpha sharpen and px_scalar are applied. Context's stack is not used. Only modifications for alpha sharpen and px_scalar are applied.
view, position, and scale are expected to be in unsigned normalized space: view, position, and scale are expected to be in unsigned normalized space:
@@ -829,8 +819,6 @@ draw_text_normalized_space :: proc( ctx : ^Context,
*/ */
// @(optimization_mode="favor_size") // @(optimization_mode="favor_size")
draw_text_shape_view_space :: #force_inline proc( ctx : ^Context, draw_text_shape_view_space :: #force_inline proc( ctx : ^Context,
font : Font_ID,
px_size : f32,
colour : RGBAN, colour : RGBAN,
view : Vec2, view : Vec2,
position : Vec2, position : Vec2,
@@ -842,21 +830,23 @@ draw_text_shape_view_space :: #force_inline proc( ctx : ^Context,
profile(#procedure) profile(#procedure)
assert( ctx != nil ) assert( ctx != nil )
// TODO(Ed): This should be taken from the shape instead (you cannot use a different font with a shape) // TODO(Ed): This should be taken from the shape instead (you cannot use a different font with a shape)
assert( font >= 0 && int(font) < len(ctx.entries) )
assert( ctx.px_scalar > 0.0 ) assert( ctx.px_scalar > 0.0 )
entry := ctx.entries[ font ] entry := ctx.entries[ shape.font ]
should_alpha_sharpen := cast(f32) cast(i32) (colour.a >= 1.0) should_alpha_sharpen := cast(f32) cast(i32) (colour.a >= 1.0)
adjusted_colour := colour adjusted_colour := colour
adjusted_colour.a += ctx.alpha_sharpen * should_alpha_sharpen adjusted_colour.a += ctx.alpha_sharpen * should_alpha_sharpen
px_scalar_quotient := (1 / ctx.px_scalar)
px_size := shape.px_size * px_scalar_quotient
resolved_size, zoom_scale := resolve_zoom_size_scale( zoom, px_size, scale, ctx.zoom_px_interval, 2, 999.0, view ) resolved_size, zoom_scale := resolve_zoom_size_scale( zoom, px_size, scale, ctx.zoom_px_interval, 2, 999.0, view )
target_position, norm_scale := get_normalized_position_scale( position, zoom_scale, view ) target_position, norm_scale := get_normalized_position_scale( position, zoom_scale, view )
// Does nothing if px_scalar is 1.0 // Does nothing if px_scalar is 1.0
target_px_size := resolved_size * ctx.px_scalar target_px_size := resolved_size * ctx.px_scalar
target_scale := norm_scale * (1 / ctx.px_scalar) target_scale := norm_scale * px_scalar_quotient
target_font_scale := parser_scale( entry.parser_info, target_px_size ) target_font_scale := parser_scale( entry.parser_info, target_px_size )
ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer,
@@ -976,12 +966,10 @@ draw_shape :: proc( ctx : ^Context, position, scale : Vec2, shape : Shaped_Text
assert( ctx.px_scalar > 0.0 ) assert( ctx.px_scalar > 0.0 )
stack := & ctx.stack stack := & ctx.stack
assert(len(stack.font) > 0)
assert(len(stack.view) > 0) assert(len(stack.view) > 0)
assert(len(stack.colour) > 0) assert(len(stack.colour) > 0)
assert(len(stack.position) > 0) assert(len(stack.position) > 0)
assert(len(stack.scale) > 0) assert(len(stack.scale) > 0)
assert(len(stack.font_size) > 0)
assert(len(stack.zoom) > 0) assert(len(stack.zoom) > 0)
// TODO(Ed): This should be taken from the shape instead (you cannot use a different font with a shape) // TODO(Ed): This should be taken from the shape instead (you cannot use a different font with a shape)
@@ -998,7 +986,9 @@ draw_shape :: proc( ctx : ^Context, position, scale : Vec2, shape : Shaped_Text
adjusted_colour := colour adjusted_colour := colour
adjusted_colour.a += ctx.alpha_sharpen * should_alpha_sharpen adjusted_colour.a += ctx.alpha_sharpen * should_alpha_sharpen
px_size := peek(stack.font_size) px_scalar_quotient := 1 / ctx.px_scalar
px_size := shape.px_size * px_scalar_quotient
zoom := peek(stack.zoom) zoom := peek(stack.zoom)
resolved_size, zoom_scale := resolve_zoom_size_scale( zoom, px_size, scale, ctx.zoom_px_interval, 2, 999.0, view ) resolved_size, zoom_scale := resolve_zoom_size_scale( zoom, px_size, scale, ctx.zoom_px_interval, 2, 999.0, view )
@@ -1010,7 +1000,7 @@ draw_shape :: proc( ctx : ^Context, position, scale : Vec2, shape : Shaped_Text
// Does nothing when px_scalar is 1.0 // Does nothing when px_scalar is 1.0
target_px_size := resolved_size * ctx.px_scalar target_px_size := resolved_size * ctx.px_scalar
target_scale := norm_scale * (1 / ctx.px_scalar) target_scale := norm_scale * px_scalar_quotient
target_font_scale := parser_scale( entry.parser_info, target_px_size ) target_font_scale := parser_scale( entry.parser_info, target_px_size )
ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer, ctx.cursor_pos = generate_shape_draw_list( & ctx.draw_list, shape, & ctx.atlas, & ctx.glyph_buffer,